数据库页布局

PostgreSQL 9.4.4 中文手册
上一页 | 上一级 | 章 59. 数据库物理存储 | 下一页

# 59.6. 数据库页布局

本节对PostgreSQL中表和索引使用的页格式进行介绍。[1] 序列表和TOAST表的格式与普通表类似。

下面说明一下，一个字节假定为包含8个比特位。另外， 术语项为存储在页上的一个独立数据值。 在表中，一项是一行；在索引中，一项为一个索引条目。

每个表和索引存储为页的数组，页的大小固定 （通常 8 kB，不过当编译服务器的时候，可以选择不同的页大小）。 在表中，所有的页是逻辑等价的，所以一个特殊项（行） 可以存储在任意页。在索引中，第一页通常保留为持有控制信息的元页， 这里可以有不同类型的索引页，这依赖于索引访问方法。

表 59-2为一个页的整体布局，每页有5部分。

表 59-2. 页整体布局

项 | 描述
--- | ---
PageHeaderData | 24字节长整型。包含关于页的一般信息，包括空闲空间指针。
ItemIdData | 指向实际项的（偏移量，长度）数组对。每项4字节。
Free space | 未分配空间。从这个区域开始分配新项指针，从结尾开始分配新项。
Items | 实际项本身
Special space | 索引访问方法专用数据。不同方法存储不同的数据。对于普通表该区域为空。

每页的前24个字节构成一个页头（PageHeaderData）。 在表 59-3有它的详细格式。 前两个字段跟踪该页最近的WAL条目。 下边的一个2字节的字段包含标志位。 随后有3个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。

表 59-3. PageHeaderData布局

字段 | 类型 | 长度 | 描述
--- | --- | --- | ---
pd_lsn | XLogRecPtr | 8字节 | LSN: 该页上最后的变化对应的xlog记录的最后字节的下一字节
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`），每个4字节。 一个项标识符包含一个到项开始的字节偏移， 其长度以字节计，和一些影响它解释的属性位。 新项标识符需要从未分配空间的开始处分配。 可以通过查看`pd_lower`来确定项标识符的数量，分配新的项标识符时， pd_lower会随之增加。因为一个项标识符从来不移动直到释放了它，即使为了紧凑空闲空间，项本身被移除，引用该项的索引可以长期被使用。实际上， PostgreSQL创建的每个指向项的指针（`ItemPointer`，也称之为`CTID`，是由页号和项标识符索引组成的。

项本身从未分配空间的结尾处开始反向存储。确切的结构取决于包含什么表。 表和序列两者都使用一个名为`HeapTupleHeaderData`的结构，下面描述。

最后的段是"特殊段"，其可以包含任何想存放访问方法。 例如，b-tree索引存储指向该页左右兄弟的连接，以及相应索引结构的一些其它数据。 普通表不使用特殊段 （通过设置`pd_special`等于页大小来表示）。

所有表的行结构相同。有个固定大小的头（在大多数机器占用23字节）， 随后一个可选的NULL位图项，对象ID字段和用户数据。 该头的详细信息见表 59-4。 实际的用户数据（行中的列）从`t_hoff`表示的偏移量开始， 它必须始终是平台的MAXALIGN的倍数。 仅在`t_infomask`设置了HEAP_HASNULL位时，NULL位图才存在。 如果它存在，它就开始于固定头的后面，占用足够的字节，每数据列一位。 （总共`t_natts`位） 在这个位列表中，1位表示非空， 0 表示空。 仅在`t_infomask`设置了HEAP_HASOID位时，对象ID 才存在。 如果存在，它将出现在t_hoff边界前。任何使 t_hoff 为MAXALIGN倍数的填充将出现在NULL位图和对象ID之间。（反过来又保证对象ID得到恰当的对齐）

表 59-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.h`可以找到所有的详细信息。

对实际数据的解析只能通过从其它表获取信息来做，大多来自于`pg_attribute`， 表示字段位置的键值是`attlen`和`attalign`。 没有直接获取特定字属性的方法，除非只有固定宽度的字段并且没有空值。 所有这些策略被封装在函数heap_getattr、fastgetattr 和heap_getsysattr中。

要读取数据你需要逐次检查每个属性。首先依据NULL位图检查字段是否为NULL， 如果是，跳到下一个，然后确定你已经右对齐；如果字段是固定宽度的， 那么所有的字节被简单地放置，如果它是变长的字段（attlen = -1） 那么它是一个更复杂的位。所有变长数据类型共享通用的头结构`struct varlena`， 其包括存储值的总长度和一些标志位。 依赖这些标志，可以判断数据是行内或在一个TOAST表，也可能是压缩的。 （参阅第 59.2 节）

### 备注

[1] | 实际上，索引访问方法不需要使用这个页格式。所有已经存在的索引方法 需要使用这些基本格式，但保存在索引元页中的数据通常不遵循这些项的布局规则。

上一页 | 起始页 | 下一页
初始化分支 | 上一级 | BKI后端接口