Database Page Layout
本节概述 PostgreSQL 表和索引内部所使用的页面格式。 [19] 序列和 TOAST 表的格式与普通表相同。
当前查看 PostgreSQL 18.6。
说明
本节概述 PostgreSQL 表和索引内部所使用的页面格式。 [19] 序列和 TOAST 表的格式与普通表相同。
- 定义范围
- 同版本核心物理存储文档
页大小在编译时确定。特殊空间由访问方法决定,普通堆表页面不使用该区域。偏移与对齐规则以所选版本的手册定义为准。
页面总体布局
| 项目 | 说明 |
|---|---|
| PageHeaderData | 长 24 字节,包含页面的通用信息,例如空闲空间指针。 |
| ItemIdData | 指向实际数据项的行指针数组。每项为一个(偏移,长度)对,占 4 字节。 |
| 空闲空间 | 尚未分配的空间。新行指针从该区域的起始处分配,新数据项从末尾处分配。 |
| 项 | 实际的数据项本身。 |
| 特殊空间 | 索引访问方法专用数据。不同方法存储的内容不同,普通表中此区域为空。 |
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;没有则为零 |
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 字节 | 用户数据的偏移量 |
手册定义
66.6. 数据库页面布局
66.6. 数据库页面布局
本节概述 PostgreSQL 表和索引内部所使用的页面格式。[19] 序列和 TOAST 表的格式与普通表相同。
在下面的说明中,假定一个字节包含 8 个位。另外,术语项指的是存储在页面上的单个数据值。在表中,项是一行;在索引中,项是一条索引条目。
每个表和索引都存储为固定大小的页面数组(通常为 8 kB,不过在编译服务器时可以选择不同的页面大小)。在表中,所有页面在逻辑上都是等价的,因此某个特定项(行)可以存放在任意页面中。在索引中,第一页通常保留为元页,用于保存控制信息;并且根据索引访问方法的不同,索引中还可能存在不同类型的页面。
表 66.2展示了页面的总体布局。每个页面包含五个部分。
表 66.2. 总体页面布局
| 项目 | 说明 |
|---|---|
| PageHeaderData | 长 24 字节,包含页面的通用信息,例如空闲空间指针。 |
| ItemIdData | 指向实际数据项的行指针数组。每项为一个(偏移,长度)对,占 4 字节。 |
| 空闲空间 | 尚未分配的空间。新行指针从该区域的起始处分配,新数据项从末尾处分配。 |
| 项 | 实际的数据项本身。 |
| 特殊空间 | 索引访问方法专用数据。不同方法存储的内容不同,普通表中此区域为空。 |
每页的前 24 字节是页首部 PageHeaderData,详细格式见表 66.3。第一个字段跟踪与该页相关的最近一条 WAL 记录;第二个字段在启用数据校验和时保存页面校验和。之后是一个包含标志位的 2 字节字段,再后面是三个 2 字节整数 pd_lower、pd_upper 和 pd_special,分别记录从页起点到未分配空间起点、未分配空间终点及特殊空间起点的字节偏移量。接下来的 2 字节 pd_pagesize_version 同时保存页大小和版本标识。PostgreSQL 8.3 起版本号为 4;8.1 与 8.2 使用 3;8.0 使用 2;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 得知;分配新标识符时它会增加。因为一个项标识符在被释放之前永远不会移动,所以即使项本身为了整理空闲空间而在页面内移动,其索引仍然可以被长期用来引用该项。事实上,每个指向项的指针(ItemPointer,也称 CTID)在 PostgreSQL 中都由页号和项标识符索引构成。
项本身存储在从未分配空间末尾开始、向后分配的区域中。其确切结构取决于表要包含什么内容。表和序列都使用一种名为 HeapTupleHeaderData 的结构体,如下所述。
最后一部分是“特殊部分”,其中可以存放访问方法希望保存的任何内容。例如,B-树索引会在这里保存指向页面左、右兄弟的链接,以及其他一些与索引结构相关的数据。普通表完全不使用特殊部分(通过将 pd_special 设为页面大小来表示)。
图 66.1展示了这些部分在页面中的布局方式。
图 66.1. 页面布局
所有表行的结构都相同。它们都有一个固定大小的头部(在大多数机器上占 23 字节),后面跟着可选的空值位图、可选的对象 ID 字段以及用户数据。头部的详细格式见表 66.4。实际用户数据(行的各列)从 t_hoff 指示的偏移位置开始,它必须始终是该平台 MAXALIGN 对齐单位的整数倍。只有当 HEAP_HASNULL 位在 t_infomask 中被置位时,空值位图才存在。若存在,它紧随固定头部之后,并占用足够多的字节,以便为每个数据列提供一位(也就是说,其位数等于 t_infomask2 中的属性个数)。在这个位图中,1 表示非空,0 表示空值。当位图不存在时,假定所有列都非空。只有当 HEAP_HASOID_OLD 位在 t_infomask 中被置位时,对象 ID 才存在。若存在,它位于 t_hoff 边界之前。为了让 t_hoff 成为 MAXALIGN 的整数倍所需的任何填充,都会出现在空值位图与对象 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。识别字段位置所需的关键值是 attlen 和 attalign。除非所有字段都是定宽且没有空值,否则没有办法直接取得某个特定属性。所有这些技巧都封装在 heap_getattr、fastgetattr 和 heap_getsysattr 这些函数中。
读取数据时,需要依次检查每个属性。首先根据空值位图判断该字段是否为 NULL。如果是,就继续下一个。然后确认对齐是否正确。如果字段是定宽字段,那么它的所有字节都是直接摆放的;如果它是变长字段(attlen = -1),情况就会稍复杂一些。所有变长数据类型都共享一个通用头部结构体 struct varlena,其中包含已存储值的总长度以及一些标志位。根据这些标志,数据可能是行内存储的,也可能位于 TOAST 表中;它也可能是经过压缩的(见第 66.2 节)。
相关条目
文档与源码
来源构建
- 版本
- 18.6
- 构建
- https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2
- 来源指纹
555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f
版本比较
PostgreSQL 17 → 18: 无变化。
比较已记录的接口与属性,排除来源指纹和构建元数据。某个样本中没有记录,不能据此判断实际引入或移除的版本。
相关条目
导出 JSON · 返回存储结构 · 收录范围为 PostgreSQL 10 至 20;最早采样版本不一定是实际引入版本。