2025年9月25日: PostgreSQL 18 发布!
支持的版本: 当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本: devel
不支持的版本: 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0

66.6. 数据库页面布局 #

本节概述了 PostgreSQL 表和索引内部使用的页面格式。[19] 序列和TOAST表与普通表格式相同。

在以下解释中,假定一个 字节 包含 8 位。此外,术语 指存储在页面上的单个数据值。在表中,一项是一行;在索引中,一项是一个索引条目。

每个表和索引都存储为固定大小(通常为 8 kB,尽管在编译服务器时可以选择不同的页面大小)的 页面 数组。在表中,所有页面在逻辑上都是等效的,因此特定的项(行)可以存储在任何页面上。在索引中,第一个页面通常保留为 元数据页 ,用于保存控制信息,并且根据索引访问方法,索引中可以存在不同类型的页面。

表 66.2 显示了页面的整体布局。每个页面有五个部分。

表 66.2. 整体页面布局

描述
PageHeaderData 24 字节长。包含关于页面的常规信息,包括空闲空间指针。
ItemIdData 指向实际项的项标识符数组。每个条目是一个(偏移量,长度)对。每项 4 字节。
Free space 未分配的空间。新的项标识符从该区域的开头分配,新的项从该区域的末尾分配。
Items 实际的项本身。
Special space 特定于索引访问方法的数据。不同的方法存储不同的数据。普通表中为空。

每个页面的前 24 字节是一个页面头(PageHeaderData)。其格式在 表 66.3 中有详细说明。第一个字段跟踪与此页面相关的最新 WAL 条目。第二个字段包含页面校验和(如果启用了 -k)。接下来是一个 2 字节的字段,包含标志位。之后是三个 2 字节的整数字段(pd_lowerpd_upperpd_special)。它们包含从页面开始到未分配空间开始、到未分配空间结束以及到特殊空间开始的字节偏移量。页面头的接下来的 2 字节 pd_pagesize_version 存储页面大小和版本指示符。从 PostgreSQL 8.3 开始,版本号为 4;PostgreSQL 8.1 和 8.2 使用版本号 3;PostgreSQL 8.0 使用版本号 2;PostgreSQL 7.3 和 7.4 使用版本号 1;之前的版本使用版本号 0。(基本页面布局和头部格式在这些版本中大部分没有改变,但堆行头部的布局发生了变化。)页面大小基本上仅作为交叉检查存在;一个安装中不支持多种页面大小。最后一个字段是一个提示,表明是否可能有效地修剪页面:它跟踪页面上最旧的未修剪 XMAX。

表 66.3. PageHeaderData 布局

字段 类型 长度 描述
pd_lsn PageXLogRecPtr 8 字节 LSN:此页面最后一次更改的 WAL 记录的最后一个字节之后的下一个字节
pd_checksum uint16 2 字节 页面校验和
pd_flags uint16 2 字节 标志位
pd_lower LocationIndex 2 字节 指向空闲空间开始的偏移量
pd_upper LocationIndex 2 字节 指向空闲空间结束的偏移量
pd_special LocationIndex 2 字节 指向特殊空间开始的偏移量
pd_pagesize_version uint16 2 字节 页面大小和布局版本号信息
pd_prune_xid TransactionId 4 字节 页面上最旧的未修剪 XMAX,如果不存在则为零

所有详细信息均可在 src/include/storage/bufpage.h 中找到。

页面头之后是项标识符(ItemIdData),每个项需要四个字节。项标识符包含项开始处的字节偏移量、其字节长度以及一些影响其解释的属性位。新项标识符根据需要从未分配空间的开头分配。项标识符的数量可以通过查看 pd_lower 来确定,该字段在分配新标识符时会增加。由于项标识符在被释放之前永远不会被移动,因此其索引可以长期用于引用项,即使项本身在页面上被移动以压缩空闲空间。事实上,PostgreSQL 创建的每个指向项的指针(ItemPointer,也称为 CTID)都由页面编号和项标识符的索引组成。

项本身存储在从未分配空间末尾向后分配的空间中。具体结构取决于表要包含的内容。表和序列都使用一个名为 HeapTupleHeaderData 的结构,下面将进行描述。

最后一个部分是 特殊部分 ,它可以包含访问方法希望存储的任何内容。例如,b-tree 索引存储指向页面左右兄弟节点的链接,以及与索引结构相关的其他一些数据。普通表根本不使用特殊部分(通过将 pd_special 设置为等于页面大小来指示)。

图 66.1 说明了这些部分在页面中的布局方式。

图 66.1. 页面布局


66.6.1. 表行布局 #

所有表行都以相同的方式构建。有一个固定大小的头部(在大多数机器上占用 23 字节),后面是可选的 NULL 位图、可选的对象 ID 字段和用户数据。头部在 表 66.4 中有详细说明。实际的用户数据(行的列)从 t_hoff 指定的偏移量开始,该偏移量必须始终是平台 MAXALIGN 距离的倍数。只有在 t_infomask 中设置了 HEAP_HASNULL 位时,才存在 NULL 位图。如果存在,它紧跟在固定头部之后,并占用足够的字节数以使每列数据(即,等于 t_infomask2 中属性计数位数的位数)有一个位。在这个位列表中,1 表示非 NULL,0 表示 NULL。当不存在位图时,假定所有列都为非 NULL。只有在 t_infomask 中设置了 HEAP_HASOID_OLD 位时,才存在对象 ID。如果存在,它出现在 t_hoff 边界之前。为了使 t_hoff 成为 MAXALIGN 的倍数而需要的任何填充将出现在 NULL 位图和对象 ID 之间。(这反过来确保了对象 ID 的对齐方式正确。)

表 66.4. HeapTupleHeaderData 布局

字段 类型 长度 描述
t_xmin TransactionId 4 字节 插入 XID 时间戳
t_xmax TransactionId 4 字节 删除 XID 时间戳
t_cid CommandId 4 字节 插入和/或删除 CID 时间戳(与 t_xvac 重叠)
t_xvac TransactionId 4 字节 移动行版本的 VACUUM 操作的 XID
t_ctid ItemPointerData 6 字节 当前行版本或更新的行版本的 TID
t_infomask2 uint16 2 字节 属性数量,以及各种标志位
t_infomask uint16 2 字节 各种标志位
t_hoff uint8 1 字节 指向用户数据的偏移量

所有详细信息均可在 src/include/access/htup_details.h 中找到。

解释实际数据只能通过从其他表(主要是 pg_attribute)获取的信息来完成。识别字段位置所需的关键值是 attlenattalign。除了只有固定宽度字段且没有 NULL 值的情况外,没有办法直接获取特定属性。所有这些技巧都封装在函数 heap_getattrfastgetattrheap_getsysattr 中。

要读取数据,您需要依次检查每个属性。首先根据 NULL 位图检查字段是否为 NULL。如果为 NULL,则继续下一个。然后确保您具有正确的对齐方式。如果字段是固定宽度的,则所有字节都按顺序放置。如果它是可变长度字段(attlen = -1),则会更复杂。所有可变长度数据类型都共享通用的结构 struct varlena,该结构包括存储值的总长度和一些标志位。根据标志位,数据可以是内联的,也可以在TOAST表中;它也可能被压缩(参见 第 66.2 节)。



[19] 实际上,表或索引访问方法不要求使用此页面格式。 heap 表访问方法始终使用此格式。所有现有的索引方法也使用基本格式,但存储在索引元数据页上的数据通常不遵循项布局规则。

提交更正

如果您在文档中发现任何不正确、与您对特定功能的体验不符或需要进一步澄清的内容,请使用 此表单 报告文档问题。