73.6. 数据库页面布局#
本节概述了PostgreSQL表和索引中使用的页面格式。[17]序列和TOAST表的格式与常规表相同。
在下面的说明中,假设一个字节包含 8 位。此外,术语项指的是存储在页面上的单个数据值。在表中,一个项是一行;在索引中,一个项是一个索引条目。
每个表和索引都存储为固定大小的页面数组(通常为 8 kB,尽管在编译服务器时可以选择不同的页面大小)。在表中,所有页面在逻辑上都是等效的,因此特定项(行)可以存储在任何页面中。在索引中,第一个页面通常保留为包含控制信息的元页面,并且索引中可以有不同类型的页面,具体取决于索引访问方法。
表 73.2显示了页面的总体布局。每个页面有五个部分。
表 73.2. 总体页面布局
项 | 描述 |
---|---|
PageHeaderData | 长度为 24 字节。包含有关页面的常规信息,包括空闲空间指针。 |
ItemIdData | 指向实际项的项标识符数组。每个条目都是一个 (偏移量、长度) 对。每个项 4 字节。 |
空闲空间 | 未分配的空间。新的项标识符从该区域的开头分配,新的项从末尾分配。 |
项 | 实际项本身。 |
特殊空间 | 索引访问方法特定数据。不同的方法存储不同的数据。在普通表中为空。 |
每页的前 24 个字节由页头(PageHeaderData
)组成。其格式在表 73.3中进行了详细说明。第一个字段跟踪与此页相关的最新 WAL 条目。如果启用了数据校验和,第二个字段将包含页校验和。接下来是一个包含标志位的 2 字节字段。后面是三个 2 字节整数字段(pd_lower
、pd_upper
和pd_special
)。这些字段包含从页开始到未分配空间开始、到未分配空间结束以及到特殊空间开始的字节偏移量。页头的下一个 2 个字节pd_pagesize_version
存储页面大小和版本指示符。从PostgreSQL8.3 开始,版本号为 4;PostgreSQL8.1 和 8.2 使用版本号 3;PostgreSQL8.0 使用版本号 2;PostgreSQL7.3 和 7.4 使用版本号 1;早期版本使用版本号 0。(基本页面布局和头格式在这些版本中大部分没有更改,但堆行头的布局已更改。)页面大小基本上只作为交叉检查而存在;不支持在安装中使用多个页面大小。最后一个字段是一个提示,显示修剪页面是否可能有利可图:它跟踪页面上最旧的未修剪 XMAX。
表 73.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
设置为等于页面大小来表示)。
图 73.1说明了这些部分如何在页面中布局。
图 73.1. 页面布局
73.6.1. 表行布局#
所有表行都以相同的方式构建。有一个固定大小的标头(在大多数机器上占用 23 个字节),后面是一个可选的空位图、一个可选的对象 ID 字段和用户数据。标头在表 73.4中进行了详细说明。实际用户数据(行的列)从t_hoff
指示的偏移量开始,该偏移量必须始终是平台的 MAXALIGN 距离的倍数。仅当HEAP_HASNULL位在t_infomask
中设置时,才会出现空位图。如果存在,它将紧跟固定标头之后,并占用足够的字节,以便每个数据列有一个位(即,等于t_infomask2
中属性计数的位数)。在此位列表中,1 位表示非空,0 位表示空。当空位图不存在时,假定所有列都非空。仅当HEAP_HASOID_OLD位在t_infomask
中设置时,才会出现对象 ID。如果存在,它将出现在t_hoff
边界之前。在空位图和对象 ID 之间将出现使t_hoff
成为 MAXALIGN 倍数所需的任何填充。(这反过来又确保了对象 ID 得到适当对齐。)
表 73.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表中;它也可能是压缩的(请参见第 73.2 节)。
[17]实际上,表或索引访问方法都不需要使用此页面格式。heap
表访问方法始终使用此格式。所有现有的索引方法也使用基本格式,但索引元页面上保留的数据通常不遵循项目布局规则。