本章提供一个PostgreSQL 的表和索引所使用的页面格式的概述.(索引访问模式不 需要使用这些页面格式。 目前,所有索引方法都使用这个基本格式,但保留在索引元数据页里 的数据通常并不准确地遵循项布局规则。)TOAST表和序列的格式和 普通表一样。
在下面解释中,假定一个 字节 包含 8 个位.另外, 项(item) 指的是存储在一个页面里的独立数据值。在一个表里, 一个项是一个元组(行);在一个索引里,一个项是一条索引记录。
Table 7-1 显示一个页面的基本布局。每个页面有五个部分。
Table 7-1. 样例页面布局
项 | 描述 |
---|---|
PageHeaderData | 20字节长。包含关于页面的一般信息, 包括自由空间指针。 |
ItemPointerData | (偏移量,长度)对的数组,指向实际项。 |
Free space(自由空间) | 未分配的空间。所有新元组都从这里分配,通常是从结尾开始。 |
Items(项) | 实际的项本身。 |
Special Space(特殊空间) | 索引访问模式相关的数据。不同的方式存放不同的数据。 在普通表中为空。 |
每个页面的头20个字节组成页头(PageHeaderData)。它的格式在 Table 7-2 里详细介绍。头两个字节 处理与 WAL 相关的东西。然后跟着三个2字节的整数字段 (pd_lower,pd_upper, 和 pd_special)。这些字段分别表示与未分配 空间开头的偏移,与未分配空间结尾的偏移,以及到特殊空间开头的偏移。
Table 7-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 里找到。
特殊空间是在页面末尾的区域,它在页面初始化的时候分配,包含与某种 访问方式相关的信息。页头的最后两个字节,pd_pagesize_version, 保存页面大小和一个版本指示器。从 PostgreSQL 7.3 开始, 版本数是 1;以前的版本使用版本号 0。(基本的页面布局以及头格式没有变化, 但是堆元组头的布局改变了。)页面大小通常只用作交叉检查;在一次安装中 没有大于一个页面尺寸的支持。
在页头后面是项标识符(ItemIdData),每个需要四个字节。 一个项标识符包含一个到项开头的字节偏移量,它自己以字节计的长度, 以及一个属性位的集合,这些属性位影响它的解释。新的项标识符根据需要 从未分配空间的开头分配。项标识符的数目可以通过查看 pd_lower 来判断,在分配新标识符的时候会递增。因为一个项标识符在其释放前绝对 不会移动,所以它的索引可以用于长时间地引用一个项,即使该项本身因为 压缩自由空间在页面内部进行了移动也如此。实际上,PostgreSQL 创建地每个指向项的指针(ItemPointer,也叫做 CTID) 都由一个页号和一个项标识符的索引组成。
项本身存储在从未分配空间末尾开始从后向前分配的空间里。它们的实际 结构因表包含的内容不同而不同。表和序列都使用一种叫做 HeapTupleHeaderData 的结构,在下面描述。
最后一段是“特殊段”,它可以包含任何访问方法想存放的东西。 普通表并不使用这个段(通过设置 pd_special 来指示,以 平衡页面大小)。
所有表元组都用同样方法构造。它们有一个定长的头(在大多数机器上占据23个字节), 后面跟着一个可选的 null 位图,一个可选的对象 ID 字段,以及用户数据。 头在 Table 7-3 里详细描述。实际用户数据 (元组的字段)从 t_hoff 标识的偏移量开始,它必须是 该平台的 MAXALIGN 距离的倍数。null 位图只有在 t_infomask 里面的 HEAP_HASNULL 位设置了的时候才出现。 如果它出现了,那么它紧跟在定长头后面,占据足够容纳每个数据字段对应一个位的字节数 (也就是说,总共 t_natts 位)。在这个位列里面,为 1 的位 表示非空,而为 0 的位表示空。如果没有出现这个位图,那么所有数据字段都 假设为非空的。对象 ID 只有在设置了 t_infomask 里面的 HEAP_HASOID 位的时候才出现。如果出现,它正好出现在 t_hoff 范围之前。如果需要补齐 t_hoff,使之 成为 MAXALIGN 的倍数,那么这些填充将出现在 null 位图和度相 ID 之间。 (这样也保证了对象 ID 得到恰当的对齐。)
Table 7-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 字节 | 这个或者新元组的当前 ID |
t_natts | int16 | 2 字节 | 字段数目 |
t_infomask | uint16 | 2 字节 | 各种标志 |
t_hoff | uint8 | 1 字节 | 到用户数据的偏移量 |
所有细节都可以在 src/include/access/htup.h 中找到。
对具体数据的解释只能在从其它表中获取的信息的情况下进行, 这些信息大多数在 pg_attribute 里。 尤其是 attlen 字段和 attalign 字段。 我们没有办法直接获取某个字段,除非它们是定宽并且没有 NULL 的。 所有这些复杂的操作都封装在函数 heap_getattr, fastgetattr 和 heap_getsysattr 里。
要读取数据的话,你需要轮流检查每个字段。首先根据 null 位图检查该字段是否为 NULL。 如果是,那么跳到下一个字段。然后保证你的对齐是正确的。如果字段是一个 定宽字段,那么所有字节都简单地放在那里。如果它是一个变长字段(attlen == -1), 那么它就会更加复杂一些,它会使用变长结构 varattrib。 根据标志地不同,数据可能是内联的,也可能是压缩的或者是在其它表中(TOAST)。