原文地址https://boringsql.com/posts/inside-the-8kb-page/文章解释了为什么是8KB、页的内部结构布局以及这些结构如何影响数据库的I/O、并发控制和清理机制。一、为什么是8KB历史渊源源自1980年代的伯克利POSTGRES项目当时选择8KB是为了匹配Unix系统的虚拟内存页4KB/8KB和磁盘扇区512字节实现高效对齐。现代权衡8KB是OLTP场景下的平衡点——太小会导致元数据开销占比过高太大会造成空间浪费和单次I/O读取过多无用数据如只读一行也要加载整个大块。对比分析型数据库如DuckDB使用更大的块如256KB以提升顺序扫描吞吐量。灵活性PostgreSQL支持在编译时通过--with-blocksize调整为1/2/4/8/16/32KB但默认8KB适用于绝大多数场景。二、页的内部结构Slotted Page 布局页采用“槽页”布局数据从两端向中间填充中间留出空闲空间页头Page Header24字节pd_lsn8字节最后修改该页的WAL日志序号用于崩溃恢复。pd_checksum2字节页数据校验和需启用。pd_flags2字节页状态标志如PD_ALL_VISIBLE页内所有行对所有事务可见是实现仅索引扫描的关键。pd_lower2字节行指针数组的结束偏移量指向空闲空间起点。pd_upper2字节元组数据的开始偏移量指向空闲空间终点。pd_special2字节特殊空间起始位置堆页为8192即无特殊空间索引页用于存储B-Tree元数据。pd_pagesize_version2字节页大小与版本。pd_prune_xid4字节用于页级剪枝允许查询在访问时就地清理死元组避免等待VACUUM。行指针Line Pointer4字节/个紧接页头向下增长相当于页的“目录”。结构lp_off元组偏移量、lp_flags状态未使用/正常/重定向/死亡、lp_len元组长度。作用索引指向的是页号行指针序号即ctid而非物理偏移。这使得页内碎片整理或HOT更新可以移动元组而索引无需变更。空闲空间位于pd_lower和pd_upper之间的区域。元组数据从页底向上增长包含元组头23字节 对齐填充 实际列数据。三、关键机制与观察容量估算每行开销 ≈ 4字节行指针 23字节元组头通常对齐到24字节 实际数据。示例表中平均每行约76字节单页可容纳约110-120行。多页扩展当数据超过单页容量PostgreSQL分配新页relpages增长。通过pageinspect可以查看每一页的填充情况如第一页只剩52字节空闲第二页只剩8字节。索引页的特殊空间堆页的pd_special8192无特殊空间。B-Tree索引页使用最后16字节存储兄弟页指针、树层级等元数据。四、使用pageinspect进行内部观察文章演示了如何通过pageinspect扩展直接查看页的原始内容page_header(get_raw_page(表, 0))解析页头信息LSN、lower/upper、特殊空间等。heap_page_items(get_raw_page(表, 0))列出页内所有行指针及对应元组长度、状态。bt_page_stats(索引, 1)查看索引页的统计信息类型、活动项、空闲大小等。五、总结页是I/O的原子单元读一行 加载整个8KB页到缓冲区。写一标志 重写整个页并记录WAL。可见性优化PD_ALL_VISIBLE标志使仅索引扫描得以跳过堆检查。轻量清理pd_prune_xid允许普通查询在访问页时触发页级剪枝回收空间。布局优势Slotted Page设计将固定大小的行指针与可变大小的元数据分离减少碎片便于管理。