本节概述了 PostgreSQL 表和索引内部使用的页面格式。[19] 序列和TOAST表与普通表格式相同。
在以下解释中,假定一个 字节 包含 8 位。此外,术语 项 指存储在页面上的单个数据值。在表中,一项是一行;在索引中,一项是一个索引条目。
每个表和索引都存储为固定大小(通常为 8 kB,尽管在编译服务器时可以选择不同的页面大小)的 页面 数组。在表中,所有页面在逻辑上都是等效的,因此特定的项(行)可以存储在任何页面上。在索引中,第一个页面通常保留为 元数据页 ,用于保存控制信息,并且根据索引访问方法,索引中可以存在不同类型的页面。
表 66.2 显示了页面的整体布局。每个页面有五个部分。
表 66.2. 整体页面布局
项 | 描述 |
---|---|
PageHeaderData | 24 字节长。包含关于页面的常规信息,包括空闲空间指针。 |
ItemIdData | 指向实际项的项标识符数组。每个条目是一个(偏移量,长度)对。每项 4 字节。 |
Free space | 未分配的空间。新的项标识符从该区域的开头分配,新的项从该区域的末尾分配。 |
Items | 实际的项本身。 |
Special space | 特定于索引访问方法的数据。不同的方法存储不同的数据。普通表中为空。 |
每个页面的前 24 字节是一个页面头(PageHeaderData
)。其格式在 表 66.3 中有详细说明。第一个字段跟踪与此页面相关的最新 WAL 条目。第二个字段包含页面校验和(如果启用了 -k
)。接下来是一个 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。
表 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
来确定,该字段在分配新标识符时会增加。由于项标识符在被释放之前永远不会被移动,因此其索引可以长期用于引用项,即使项本身在页面上被移动以压缩空闲空间。事实上,PostgreSQL 创建的每个指向项的指针(ItemPointer
,也称为 CTID
)都由页面编号和项标识符的索引组成。
项本身存储在从未分配空间末尾向后分配的空间中。具体结构取决于表要包含的内容。表和序列都使用一个名为 HeapTupleHeaderData
的结构,下面将进行描述。
最后一个部分是 “特殊部分” ,它可以包含访问方法希望存储的任何内容。例如,b-tree 索引存储指向页面左右兄弟节点的链接,以及与索引结构相关的其他一些数据。普通表根本不使用特殊部分(通过将 pd_special
设置为等于页面大小来指示)。
图 66.1 说明了这些部分在页面中的布局方式。
图 66.1. 页面布局
所有表行都以相同的方式构建。有一个固定大小的头部(在大多数机器上占用 23 字节),后面是可选的 NULL 位图、可选的对象 ID 字段和用户数据。头部在 表 66.4 中有详细说明。实际的用户数据(行的列)从 t_hoff
指定的偏移量开始,该偏移量必须始终是平台 MAXALIGN 距离的倍数。只有在 t_infomask
中设置了 HEAP_HASNULL 位时,才存在 NULL 位图。如果存在,它紧跟在固定头部之后,并占用足够的字节数以使每列数据(即,等于 t_infomask2
中属性计数位数的位数)有一个位。在这个位列表中,1 表示非 NULL,0 表示 NULL。当不存在位图时,假定所有列都为非 NULL。只有在 t_infomask
中设置了 HEAP_HASOID_OLD 位时,才存在对象 ID。如果存在,它出现在 t_hoff
边界之前。为了使 t_hoff
成为 MAXALIGN 的倍数而需要的任何填充将出现在 NULL 位图和对象 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
。除了只有固定宽度字段且没有 NULL 值的情况外,没有办法直接获取特定属性。所有这些技巧都封装在函数 heap_getattr、fastgetattr 和 heap_getsysattr 中。
要读取数据,您需要依次检查每个属性。首先根据 NULL 位图检查字段是否为 NULL。如果为 NULL,则继续下一个。然后确保您具有正确的对齐方式。如果字段是固定宽度的,则所有字节都按顺序放置。如果它是可变长度字段(attlen = -1),则会更复杂。所有可变长度数据类型都共享通用的结构 struct varlena
,该结构包括存储值的总长度和一些标志位。根据标志位,数据可以是内联的,也可以在TOAST表中;它也可能被压缩(参见 第 66.2 节)。
如果您在文档中发现任何不正确、与您对特定功能的体验不符或需要进一步澄清的内容,请使用 此表单 报告文档问题。