本节概述了 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
)。这些字段包含从页开始到未分配空间开始、未分配空间末尾和特殊空间开头的字节偏移量。页头的后 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。
表 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 个字节),后跟一个可选的空位图、一个可选的对象 ID 字段以及用户数据。标题详见 表 65.4。实际用户数据(行的列)从 t_hoff
指示的偏移量开始,而 t_hoff
必须始终是针对平台的最大对齐距离的倍数。仅当 HEAP_HASNULL 位在 t_infomask
中设置时,空位图才存在。如果存在,它会紧跟固定头之后,并且占据足够多的字节,以对每个数据列有一个位(即,位数等于 t_infomask2
中的属性计数)。在这个位列表中,1 位表示非空,0 位表示空。当位图不存在时,所有列均假定为非空。仅当 HEAP_HASOID_OLD 位在 t_infomask
中设置时,对象 ID 才存在。如果存在,则恰好出现在 t_hoff
边界之前。为了使 t_hoff
成为最大对齐的倍数所需要的任何填充都将出现在空位图和对象 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 节)。