第 51 章 页文件
摘要
对数据库文件页面格式的说明。
本节概述 PostgreSQL 表和索引内部所使用的页面格式。(索引访问方法不要求必须使用这种页面格式。现有的所有索引方法都使用这种基本格式,但索引元页中保存的数据通常并不完全遵循项布局规则。)TOAST 表和序列的格式与普通表相同。
在下面的说明中,假定一个字节包含 8 个位。另外,术语项指的是存储在页面上的单个数据值。在表中,项是一行;在索引中,项是一条索引条目。
表 51.1展示了页面的基本布局。每个页面包含五个部分。
表 51.1. 页面布局示例
| 项 | 描述 |
|---|---|
| PageHeaderData | 长度为 20 字节。包含页面的一般信息,包括空闲空间指针。 |
| ItemIdData | 指向实际项的(偏移量、长度)对数组。 |
| 空闲空间 | 尚未分配的空间。所有新行都从这里分配,通常从末尾开始。 |
| 项 | 项本身。 |
| 特殊空间 | 索引访问方法特有的数据。不同方法存储不同数据。普通表中为空。 |
每个页面的前 20 个字节由页头(PageHeaderData)组成。它的格式详见表 51.2。前两个字段处理与 WAL
有关的内容。接下来是三个 2 字节整数字段(pd_lower, pd_upper,
和 pd_special)。它们分别表示到未分配空间起始位置、到未分配空间结束位置以及到特殊空间起始位置的字节偏移量。
表 51.2. PageHeaderData 布局
| 字段 | 类型 | 长度 | 描述 |
|---|---|---|---|
| pd_lsn | XLogRecPtr | 8 字节 | LSN:xlog 最后一个字节之后的下一个字节 |
| pd_sui | StartUpID | 4 字节 | 最近更改的 SUI(目前只有堆 AM 使用它) |
| pd_lower | LocationIndex | 2 字节 | 到空闲空间起始位置的偏移量。 |
| pd_upper | LocationIndex | 2 字节 | 到空闲空间结束位置的偏移量。 |
| pd_special | LocationIndex | 2 字节 | 到特殊空间起始位置的偏移量。 |
| pd_pagesize_version | uint16 | 2 字节 | 页面大小和布局版本号信息。 |
所有细节都可以在 src/include/storage/bufpage.h 中找到。
特殊空间是页面末尾的一个区域,它在页面初始化时分配,并包含某种访问方法特有的信息。页头的最后 2 个字节
pd_pagesize_version 同时存储页面大小和一个版本指示器。从
PostgreSQL 7.3 起版本号为 1;更早的版本使用版本号 0。(基本页面布局和页头格式并未改变,但堆行头部的布局发生过变化。)页面大小基本上只是用于交叉检查;一个安装中并不支持同时存在多种页面大小。
页头之后是项标识符(ItemIdData),每个需要四个字节。一个项标识符包含项起始位置的字节偏移量、其字节长度,以及若干影响解释方式的属性位。新的项标识符会根据需要从未分配空间的起始处分配。当前已有多少个项标识符,可以通过查看
pd_lower 得知;分配新标识符时它会增加。因为一个项标识符在被释放之前永远不会移动,所以即使项本身为了压缩空闲空间而在页面内移动,其索引仍然可以被长期用来引用该项。事实上,每个指向项的指针(ItemPointer,也称
CTID)在
PostgreSQL 中都由页号和项标识符索引构成。
项本身存储在从未分配空间末尾开始、向后分配的区域中。其确切结构取决于表要包含什么内容。表和序列都使用一种名为
HeapTupleHeaderData 的结构体,如下所述。
最后一部分是“特殊部分”,其中可以存放访问方法希望保存的任何内容。普通表完全不使用特殊部分(通过将
pd_special 设为等于页面大小来表示)。
所有表行的结构都相同。它们都有一个固定大小的头部(在大多数机器上占 23 字节),后面跟着可选的空值位图、可选的对象 ID 字段以及用户数据。头部的详细格式见表 51.3。实际用户数据(行的各列)从
t_hoff 指示的偏移位置开始,它必须始终是该平台
MAXALIGN 对齐单位的整数倍。只有当 HEAP_HASNULL 位在
t_infomask 中被置位时,空值位图才存在。若存在,它紧随固定头部之后,并占用足够多的字节,以便为每个数据列提供一位(也就是说,总共为 t_natts 位)。在这个位图中,1 表示非空,0 表示空值。当位图不存在时,假定所有列都非空。只有当 HEAP_HASOID 位在 t_infomask 中被置位时,对象 ID 才存在。若存在,它位于
t_hoff 边界之前。为了让 t_hoff
成为 MAXALIGN 的整数倍所需的任何填充,都会出现在空值位图与对象 ID 之间。(这又反过来保证了对象 ID 的对齐是合适的。)
表 51.3. HeapTupleHeaderData 布局
| 字段 | 类型 | 长度 | 描述 |
|---|---|---|---|
| t_xmin | TransactionId | 4 字节 | 插入 XID 戳 |
| t_cmin | CommandId | 4 字节 | 插入 CID 戳(与 t_xmax 共用存储) |
| t_xmax | TransactionId | 4 字节 | 删除 XID 戳 |
| t_cmax | CommandId | 4 字节 | 删除 CID 戳(与 t_xvac 共用存储) |
| t_xvac | TransactionId | 4 字节 | VACUUM 操作移动某个行版本时的 XID |
| t_ctid | ItemPointerData | 6 字节 | 本行版本或更新行版本的当前 TID |
| t_natts | int16 | 2 字节 | 属性数量 |
| t_infomask | uint16 | 2 字节 | 各种标志位 |
| t_hoff | uint8 | 1 字节 | 到用户数据的偏移量 |
所有细节都可以在 src/include/access/htup.h 中找到。
要解释实际数据,必须借助从其他表中获得的信息,其中大部分来自
pg_attribute。所需的特定字段是
attlen 和
attalign。除非所有字段都是定宽且没有空值,否则没有办法直接取得某个特定属性。所有这些技巧都封装在
heap_getattr、fastgetattr
和 heap_getsysattr
这些函数中。
读取数据时,需要依次检查每个属性。首先根据空值位图判断该字段是否为
NULL。如果是,就继续下一个。然后确认对齐是否正确。如果字段是定宽字段,那么它的所有字节都是直接摆放的;如果它是变长字段(attlen == -1),情况就会稍复杂一些,需要使用变长结构 varattrib。根据标志位的不同,数据可能是线内的、经过压缩的,或者位于另一个表中(TOAST)。