↑↓ 选择↵ 打开⌫ 切换范围完整搜索

PG.CENTER 连接 PostgreSQL 文档、百科与生态知识。由 Pigsty 维护。

支持中的版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发中的版本: 19 / 20devel
已结束支持的版本: 13 / 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
历史版本。 PostgreSQL 13 已结束支持。 2025-11-13. 请参阅 当前版本手册.

69.6. 数据库页面布局 #

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

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

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

表 69.2 展示了页面的总体布局。每个页面包含五个部分。

表 69.2. 总体页面布局

项 描述
PageHeaderData长度为 24 字节。包含页面的一般信息,包括空闲空间指针。
ItemIdData指向实际项的项标识符数组。每个条目都是一个(偏移量、长度)对。每项 4 字节。
空闲空间尚未分配的空间。新的项标识符从该区域起始处分配,新的项从末尾处分配。
项项本身。
特殊空间索引访问方法特有的数据。不同方法存储不同数据。普通表中为空。

每个页面的前 24 个字节由页头(PageHeaderData)组成。它的格式详见表 69.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。

表 69.3. PageHeaderData 布局

字段类型长度描述
pd_lsnPageXLogRecPtr8 字节LSN:上次修改此页面的 WAL 记录最后一个字节之后的下一个字节
pd_checksumuint162 字节页面校验和
pd_flagsuint162 字节标志位
pd_lowerLocationIndex2 字节到空闲空间起始位置的偏移量
pd_upperLocationIndex2 字节到空闲空间结束位置的偏移量
pd_specialLocationIndex2 字节到特殊空间起始位置的偏移量
pd_pagesize_versionuint162 字节页面大小和布局版本号信息
pd_prune_xidTransactionId4 字节页面上最老的未剪枝 XMAX,若无则为零

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

页头之后是项标识符(ItemIdData),每个需要四个字节。一个项标识符包含项起始位置的字节偏移量、其字节长度,以及若干影响解释方式的属性位。新的项标识符会根据需要从未分配空间的起始处分配。当前已有多少个项标识符,可以通过查看 pd_lower 得知;分配新标识符时它会增加。因为一个项标识符在被释放之前永远不会移动,所以即使项本身为了整理空闲空间而在页面内移动,其索引仍然可以被长期用来引用该项。事实上,每个指向项的指针(ItemPointer,也称 CTID)在 PostgreSQL 中都由页号和项标识符索引构成。

项本身存储在从未分配空间末尾开始、向后分配的区域中。其确切结构取决于表要包含什么内容。表和序列都使用一种名为 HeapTupleHeaderData 的结构体,如下所述。

最后一部分是“特殊部分”,其中可以存放访问方法希望保存的任何内容。例如,B-树索引会在这里保存指向页面左、右兄弟的链接,以及其他一些与索引结构相关的数据。普通表完全不使用特殊部分(通过将 pd_special 设为页面大小来表示)。

图 69.1 展示了这些部分在页面中的布局方式。

图 69.1. 页面布局


69.6.1. 表行布局 #

所有表行的结构都相同。它们都有一个固定大小的头部(在大多数机器上占 23 字节),后面跟着可选的空值位图、可选的对象 ID 字段以及用户数据。头部的详细格式见表 69.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 的对齐是合适的。)

表 69.4. HeapTupleHeaderData 布局

字段类型长度描述
t_xminTransactionId4 字节插入 XID 标记
t_xmaxTransactionId4 字节删除 XID 标记
t_cidCommandId4 字节插入和/或删除 CID 标记(与 t_xvac 重叠)
t_xvacTransactionId4 字节VACUUM 操作移动某个行版本时的 XID
t_ctidItemPointerData6 字节本行版本或较新的行版本的当前 TID
t_infomask2uint162 字节属性个数以及各种标志位
t_infomaskuint162 字节各种标志位
t_hoffuint81 字节到用户数据的偏移量

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

要解释实际数据,必须借助从其他表中获得的信息,其中大部分来自 pg_attribute。识别字段位置所需的关键值是 attlen 和 attalign。除非所有字段都是定宽且没有空值,否则没有办法直接取得某个特定属性。所有这些技巧都封装在 heap_getattr、fastgetattr 和 heap_getsysattr 这些函数中。

读取数据时,需要依次检查每个属性。首先根据空值位图判断该字段是否为 NULL。如果是,就继续下一个。然后确认对齐是否正确。如果字段是定宽字段,那么它的所有字节都是直接摆放的;如果它是变长字段(attlen = -1),情况就会稍复杂一些。所有变长数据类型都共享一个通用头部结构体 struct varlena,其中包含已存储值的总长度以及一些标志位。根据这些标志,数据可能是行内存储的,也可能位于 TOAST 表中;它也可能是经过压缩的(见第 69.2 节)。



[15] 实际上,表访问方法和索引访问方法都不要求必须使用这种页面格式。heap 表访问方法始终使用这种格式。现有的所有索引方法也都使用这种基本格式,但索引元页中保存的数据通常并不遵循项布局规则。

报告文档问题

阅读 上游文档. 反馈更正前请先核对 当前版本手册.