PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

66.6. 数据库页面布局 #

66.6.1. 表行布局

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

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

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

Table 66.2 展示了一个页面的整体布局。 每个页面有五个部分。

Table 66.2. Overall Page Layout

Item Description
PageHeaderData24 字节长。包含关于页面的一般信息,包括空闲空间指针。
ItemIdData指向实际条目的条目标识符数组。每个条目是一个(偏移量,长度)对。每个条目占 4 字节。
Free space未分配空间。新的条目标识符从这个区域的起点开始分配,新的条目则从末端开始分配。
Items实际的条目本身。
Special space索引访问方法特定的数据。不同的方法存储不同的数据。在普通表中为空。

每个页面的头 24 个字节构成页面头部 (PageHeaderData)。其格式在 Table 66.3 中详述。第一个字段跟踪与此 页面相关的最近的 WAL 条目。第二个字段在 数据校验和启用时包含页面校验和。 接下来是一个包含标志位的 2 字节字段。之后是三个 2 字节整数字段 (pd_lower、 pd_upper 和 pd_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。

Table 66.3. PageHeaderData Layout

FieldTypeLengthDescription
pd_lsnPageXLogRecPtr8 bytesLSN:本页面最后一次修改所对应 WAL 记录最后一个字节之后的字节
pd_checksumuint162 bytesPage checksum
pd_flagsuint162 bytesFlag bits
pd_lowerLocationIndex2 bytes到空闲空间起点的偏移量
pd_upperLocationIndex2 bytes到空闲空间终点的偏移量
pd_specialLocationIndex2 bytes到特殊空间起点的偏移量
pd_pagesize_versionuint162 bytes页面大小和布局版本号信息
pd_prune_xidTransactionId4 bytes页面上最早的未修剪 XMAX,如果没有则为零

所有细节都可以在 src/include/storage/bufpage.h 中找到。

页面头部之后是条目标识符(ItemIdData),每个占四个 字节。 条目标识符包含到一个条目起点的字节偏移、以字节计的长度,以及影响其 解释方式的几个属性位。 新的条目标识符根据需要从未分配空间的起点开始分配。 现有条目标识符的数量可以通过查看 pd_lower 确定,分配新标识符时该值会增大。 由于条目标识符在被释放之前从不移动,它的索引可以被长期用来引用一个 条目,即使条目本身在页面上被移动以压缩空闲空间。事实上, PostgreSQL 创建的每个指向条目的指针 (ItemPointer,也称为 CTID)都由一个页码和一个条目标识符的索引 构成。

条目本身存储在从未分配空间末端反向分配的空间中。其确切结构取决于表 要存储的内容。表和序列都使用一种名为 HeapTupleHeaderData 的结构,下文将对其进行描述。

最后一部分是“特殊部分”,访问方法可以把任何想存储的内容 放在那里。例如,b-tree 索引会存储指向该页面左右兄弟页面的链接,以及 一些与索引结构相关的其他数据。 普通表根本不使用特殊部分(通过把 pd_special 设为等于页面大小来表示)。

Figure 66.1 展示了这些部分在一个 页面中的布局。

Figure 66.1. Page Layout


66.6.1. 表行布局 #

所有表行的结构都相同。有一个固定大小的头部(在大多数机器上占 23 字节),其后是一个可选的空位图、一个可选的对象 ID 字段以及用户数据。 头部的细节见 Table 66.4。实际的用户数据(行的 各列)从 t_hoff 指示的偏移开始,该偏移 必须始终是平台 MAXALIGN 距离的倍数。 只有当 t_infomask 中设置了 HEAP_HASNULL 位时,空位图才存在。 如果存在,它就紧跟在固定头部之后,占用足够的字节以便每个数据列有一个 位(即位数等于 t_infomask2 中的属性 数)。在这个位列表中,位为 1 表示非空,位为 0 表示空。 当位图不存在时,所有列都被假定为非空。 只有当 t_infomask 中设置了 HEAP_HASOID_OLD 位时,对象 ID 才存在。 如果存在,它出现在 t_hoff 边界之前。 使 t_hoff 成为 MAXALIGN 倍数所需的任何 填充都会出现在空位图与对象 ID 之间。 (这反过来确保了对象 ID 得到合适的对齐。)

Table 66.4. HeapTupleHeaderData Layout

FieldTypeLengthDescription
t_xminTransactionId4 bytesinsert XID stamp
t_xmaxTransactionId4 bytesdelete XID stamp
t_cidCommandId4 bytes插入和/或删除 CID 标记(与 t_xvac 重叠)
t_xvacTransactionId4 bytes移动行版本的 VACUUM 操作的 XID
t_ctidItemPointerData6 bytes本行版本或更新行版本的当前 TID
t_infomask2uint162 bytes属性数量,外加各种标志位
t_infomaskuint162 bytesvarious flag bits
t_hoffuint81 byte到用户数据的偏移量

所有细节都可以在 src/include/access/htup_details.h 中找到。

解释实际数据只能借助从其他表(主要是 pg_attribute)获得的信息完成。 标识字段位置所需的关键值是 attlen 和 attalign。 无法直接取得某个特定的属性,除非表中只有定宽字段且没有空值。 所有这些技巧都封装在函数 heap_getattr、fastgetattr 和 heap_getsysattr 中。

要读取数据,您需要依次检查每个属性。首先根据空位图检查该字段是否为 NULL。如果是,就跳到下一个。然后确保对齐正确。 如果该字段是定宽字段,那么所有字节都直接就位。 如果它是变长字段(attlen = -1),情况就稍微复杂一些。 所有变长数据类型共享公共的头部结构 struct varlena,其中包含被存储值的总长度和一些标志位。 根据这些标志,数据可以在行内,也可以在 TOAST 表 中;它还可能被压缩(见 Section 66.2)。



[13] 实际上,无论是表访问方法还是索引访问方法都不强制使用这种页面 格式。heap 表访问方法总是使用这种格式。 现有的所有索引方法也都使用这一基本格式,但索引元页面上保存的数据 通常不遵循条目布局规则。