本节概述 PostgreSQL 表和索引中使用的页面 格式。[13] 序列和 TOAST 表的格式与普通表相同。
在下面的解释中,假定 字节 包含 8 个位。此外,术语 条目 指存储在页面上的单个数据值。在表中,条目就是一行;在索引中,条目就是 一个索引项。
每个表和索引都存储为固定大小 页面的数组(通常 8 kB,尽管可以在编译服务器时 选择不同的页面大小)。在表中,所有页面在逻辑上都是等价的,因此特定 的条目(行)可以存储在任何页面中。在索引中,第一页通常被保留为保存 控制信息的元页面,并且根据索引访问方法的不同, 索引内可以存在不同类型的页面。
Table 66.2 展示了一个页面的整体布局。 每个页面有五个部分。
Table 66.2. Overall Page Layout
| Item | Description |
|---|---|
| PageHeaderData | 24 字节长。包含关于页面的一般信息,包括空闲空间指针。 |
| ItemIdData | 指向实际条目的条目标识符数组。每个条目是一个(偏移量,长度)对。每个条目占 4 字节。 |
| Free space | 未分配空间。新的条目标识符从这个区域的起点开始分配,新的条目则从末端开始分配。 |
| Items | 实际的条目本身。 |
| Special space | 索引访问方法特定的数据。不同的方法存储不同的数据。在普通表中为空。 |
每个页面的头 24 个字节构成页面头部
(PageHeaderData)。其格式在
Table 66.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。
Table 66.3. PageHeaderData Layout
| Field | Type | Length | Description |
|---|---|---|---|
| pd_lsn | PageXLogRecPtr | 8 bytes | LSN:本页面最后一次修改所对应 WAL 记录最后一个字节之后的字节 |
| pd_checksum | uint16 | 2 bytes | Page checksum |
| pd_flags | uint16 | 2 bytes | Flag bits |
| pd_lower | LocationIndex | 2 bytes | 到空闲空间起点的偏移量 |
| pd_upper | LocationIndex | 2 bytes | 到空闲空间终点的偏移量 |
| pd_special | LocationIndex | 2 bytes | 到特殊空间起点的偏移量 |
| pd_pagesize_version | uint16 | 2 bytes | 页面大小和布局版本号信息 |
| pd_prune_xid | TransactionId | 4 bytes | 页面上最早的未修剪 XMAX,如果没有则为零 |
所有细节都可以在
src/include/storage/bufpage.h 中找到。
页面头部之后是条目标识符(ItemIdData),每个占四个
字节。
条目标识符包含到一个条目起点的字节偏移、以字节计的长度,以及影响其
解释方式的几个属性位。
新的条目标识符根据需要从未分配空间的起点开始分配。
现有条目标识符的数量可以通过查看
pd_lower 确定,分配新标识符时该值会增大。
由于条目标识符在被释放之前从不移动,它的索引可以被长期用来引用一个
条目,即使条目本身在页面上被移动以压缩空闲空间。事实上,
PostgreSQL 创建的每个指向条目的指针
(ItemPointer,也称为
CTID)都由一个页码和一个条目标识符的索引
构成。
条目本身存储在从未分配空间末端反向分配的空间中。其确切结构取决于表
要存储的内容。表和序列都使用一种名为
HeapTupleHeaderData 的结构,下文将对其进行描述。
最后一部分是“特殊部分”,访问方法可以把任何想存储的内容
放在那里。例如,b-tree 索引会存储指向该页面左右兄弟页面的链接,以及
一些与索引结构相关的其他数据。
普通表根本不使用特殊部分(通过把
pd_special 设为等于页面大小来表示)。
Figure 66.1 展示了这些部分在一个 页面中的布局。
Figure 66.1. Page Layout
所有表行的结构都相同。有一个固定大小的头部(在大多数机器上占 23
字节),其后是一个可选的空位图、一个可选的对象 ID 字段以及用户数据。
头部的细节见
Table 66.4。实际的用户数据(行的
各列)从 t_hoff 指示的偏移开始,该偏移
必须始终是平台 MAXALIGN 距离的倍数。
只有当 t_infomask 中设置了
HEAP_HASNULL 位时,空位图才存在。
如果存在,它就紧跟在固定头部之后,占用足够的字节以便每个数据列有一个
位(即位数等于 t_infomask2 中的属性
数)。在这个位列表中,位为 1 表示非空,位为 0 表示空。
当位图不存在时,所有列都被假定为非空。
只有当 t_infomask 中设置了
HEAP_HASOID_OLD 位时,对象 ID 才存在。
如果存在,它出现在 t_hoff 边界之前。
使 t_hoff 成为 MAXALIGN 倍数所需的任何
填充都会出现在空位图与对象 ID 之间。
(这反过来确保了对象 ID 得到合适的对齐。)
Table 66.4. HeapTupleHeaderData Layout
| Field | Type | Length | Description |
|---|---|---|---|
| t_xmin | TransactionId | 4 bytes | insert XID stamp |
| t_xmax | TransactionId | 4 bytes | delete XID stamp |
| t_cid | CommandId | 4 bytes | 插入和/或删除 CID 标记(与 t_xvac 重叠) |
| t_xvac | TransactionId | 4 bytes | 移动行版本的 VACUUM 操作的 XID |
| t_ctid | ItemPointerData | 6 bytes | 本行版本或更新行版本的当前 TID |
| t_infomask2 | uint16 | 2 bytes | 属性数量,外加各种标志位 |
| t_infomask | uint16 | 2 bytes | various flag bits |
| t_hoff | uint8 | 1 byte | 到用户数据的偏移量 |
所有细节都可以在
src/include/access/htup_details.h 中找到。
解释实际数据只能借助从其他表(主要是
pg_attribute)获得的信息完成。
标识字段位置所需的关键值是
attlen 和 attalign。
无法直接取得某个特定的属性,除非表中只有定宽字段且没有空值。
所有这些技巧都封装在函数
heap_getattr、fastgetattr
和 heap_getsysattr 中。
要读取数据,您需要依次检查每个属性。首先根据空位图检查该字段是否为
NULL。如果是,就跳到下一个。然后确保对齐正确。
如果该字段是定宽字段,那么所有字节都直接就位。
如果它是变长字段(attlen = -1),情况就稍微复杂一些。
所有变长数据类型共享公共的头部结构
struct varlena,其中包含被存储值的总长度和一些标志位。
根据这些标志,数据可以在行内,也可以在 TOAST 表
中;它还可能被压缩(见 Section 66.2)。
[13]
实际上,无论是表访问方法还是索引访问方法都不强制使用这种页面
格式。heap 表访问方法总是使用这种格式。
现有的所有索引方法也都使用这一基本格式,但索引元页面上保存的数据
通常不遵循条目布局规则。