本节概述了 PostgreSQL 表格和索引中使用的页面格式。[17] 序列和TOAST表的格式与普通表相同。
在以下解释中,假设一个字节包含 8 位。此外,术语项是指存储在页面上的单个数据值。在表中,项是一行;在索引中,项是一个索引条目。
每个表和索引都存储为固定大小(通常为 8 kB,尽管在编译服务器时可以选择不同的页面大小)的页面数组。在表中,所有页面在逻辑上是等效的,因此特定的项(行)可以存储在任何页面中。在索引中,第一页通常保留为保存控制信息的元页面,并且索引中可能存在不同类型的页面,具体取决于索引访问方法。
表 65.2 显示了页面的整体布局。每个页面分为五个部分。
表 65.2. 整体页面布局
项 | 描述 |
---|---|
PageHeaderData | 24 字节长。包含有关页面的常规信息,包括可用空间指针。 |
ItemIdData | 指向实际项的项标识符数组。每个条目都是一个(偏移量,长度)对。每个项 4 个字节。 |
可用空间 | 未分配的空间。新的项标识符从该区域的开头分配,新的项从结尾分配。 |
项 | 实际的项本身。 |
特殊空间 | 索引访问方法特定的数据。不同的方法存储不同的数据。在普通表中为空。 |
每个页面的前 24 个字节由页面头 (PageHeaderData
) 组成。其格式在表 65.3 中详细说明。第一个字段跟踪与此页面相关的最新 WAL 条目。如果启用数据校验和,则第二个字段包含页面校验和。接下来是一个包含标志位的 2 字节字段。之后是三个 2 字节整数字段(pd_lower
、pd_upper
和 pd_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. 页面布局
所有表行的结构都相同。有一个固定大小的头(在大多数机器上占用 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
)获得的信息来完成。识别字段位置所需的关键值是 attlen
和 attalign
。除了当只有固定宽度字段且没有空值时,没有办法直接获取特定的属性。所有这些技巧都封装在函数 heap_getattr、fastgetattr 和 heap_getsysattr 中。
要读取数据,你需要依次检查每个属性。首先根据空位图检查该字段是否为 NULL。如果是,则转到下一个。然后确保你有正确的对齐方式。如果该字段是固定宽度字段,则所有字节都被简单地放置。如果它是一个可变长度字段(attlen = -1),则会更复杂一些。所有可变长度数据类型都共享公共头结构 struct varlena
,其中包括存储值的总长度和一些标志位。根据标志位,数据可以在内联或在TOAST表中;它也可能被压缩(参见 第 65.2 节)。
如果您发现文档中有任何不正确、与您使用特定功能的经验不符或需要进一步澄清的地方,请使用 此表格 报告文档问题。