支持的版本: 当前 (17) / 16 / 15 / 14 / 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

65.6. 数据库页面布局 #

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

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

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

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

表 65.2. 整体页面布局

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

每个页面的前 24 个字节由页面头 (PageHeaderData) 组成。其格式在表 65.3 中详细说明。第一个字段跟踪与此页面相关的最新 WAL 条目。如果启用数据校验和,则第二个字段包含页面校验和。接下来是一个包含标志位的 2 字节字段。之后是三个 2 字节整数字段(pd_lowerpd_upperpd_special)。这些字段包含从页面开始到未分配空间开始、到未分配空间结尾以及到特殊空间开始的字节偏移量。页面头的下两个字节 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。

表 65.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 树索引存储指向页面左侧和右侧兄弟节点的链接,以及一些与索引结构相关的其他数据。普通表根本不使用特殊部分(通过将 pd_special 设置为等于页面大小来表示)。

图 65.1 说明了这些部分如何在页面中布局。

图 65.1. 页面布局


65.6.1. 表行布局 #

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

表 65.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。除了当只有固定宽度字段且没有空值时,没有办法直接获取特定的属性。所有这些技巧都封装在函数 heap_getattrfastgetattrheap_getsysattr 中。

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



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

提交更正

如果您发现文档中有任何不正确、与您使用特定功能的经验不符或需要进一步澄清的地方,请使用 此表格 报告文档问题。