1.3 堆表文件的内部布局
数据文件(堆表、索引,也包括空闲空间映射和可见性映射)内部被划分为固定长度的页,或者叫区块,大小默认为8192B(8KB)。每个文件中的页从0开始按顺序编号,这些数字称为区块号。如果文件已填满,PostgreSQL就通过在文件末尾追加一个新的空页来增加文件长度。
页面内部的布局取决于数据文件的类型。本节会描述表的页面布局,因为理解接下来的几章内容需要用到这些知识。图1.4就是堆表文件的页面布局。
图1.4 堆表文件的页面布局
表的页面包含了三种类型的数据:
1.堆元组——即数据记录本身。它们从页面底部开始依序堆叠。第5.2节与第9章会描述元组的内部结构,这一知识对于理解PostgreSQL并发控制与WAL机制是必备的。
2.行指针——每个行指针占4B,保存着指向堆元组的指针。它们也被称为项目指针。行指针形成一个简单的数组,扮演了元组索引的角色。每个索引项从1开始依次编号,称为偏移号。当向页面中添加新元组时,一个相应的新行指针也会被放入数组中,并指向新添加的元组。
3.首部数据——页面的起始位置分配了由结构PageHeaderData 定义的首部数据。它的大小为24B,包含关于页面的元数据。该结构的主要成员变量如下。
· pd_lsn——本页面最近一次变更所写入的XLOG记录对应的LSN。它是一个8B无符号整数,与WAL机制相关,第9章将详细展开介绍。
· pd_checksum——本页面的校验和值。(注意,只有在9.3或更高版本中才有此变量,早期版本中该字段用于存储页面的时间线标识。)
· pd_lower、pd_upper——pd_lower 指向行指针的末尾,pd_upper 指向最新堆元组的起始位置。
· pd_special——在索引页中会用到该字段,在堆表页中它指向页尾。(在索引页中它指向特殊空间的起始位置,特殊空间是仅由索引使用的特殊数据区域,包含特定的数据,具体内容依索引的类型而定,如B树、GiST、GiN等。)
/* @src/include/storage/bufpage.h */ /* * 磁盘页面布局 * * 对任何页面都适用的通用空间管理信息 ** pd_lsn - 本页面最近变更对应xlog记录的标识 * pd_checksum - 页面校验和 * pd_flags - 标记位 * pd_lower - 空闲空间开始位置 * pd_upper - 空闲空间结束位置 * pd_special - 特殊空间开始位置 * pd_pagesize_version - 页面的大小,以及页面布局的版本号 * pd_prune_xid - 本页面中可以修剪的最老的元组中的XID * * 缓冲管理器使用LSN来强制实施WAL的基本规则是“WAL要先于数据写入”。在xlog刷盘位置超过 * 本页面的LSN之前,不允许将缓冲区的脏页刷入磁盘 * * pd_checksum 存储着页面的校验和,如果本页面配置了校验,0就是一个合法的校验和值。如果页面 * 没有使用校验和,我们就不会设置这个字段的值;通常这意味着该字段值为0,但如果数据库从早于 * 9.3版本的pg_upgrade升级而来,也可能会出现非零的值。因为那时候这块地方用于存储页面最后 * 更新时的时间线标识。注意,并没有标识告诉你页面的标识符到底是有效的还是无效的,也没有与之关 * 联的标记。这是特意设计的,从而避免了依赖页面的具体内容来决定是否校验页面本身 * * pd_prune_xid是一个提示字段,用于帮助确认剪枝是否有用。目前索引页没用 * * 页面版本编号与页面尺寸被打包成了单个uint16字段,这是有历史原因的:在PostgreSQL7.3之前 * 并没有页面版本编号这个概念,这样做能让我们假装7.3之前版本的页面版本编号为0。我们约定页面 * 的尺寸必须为256的倍数,留下低8位用于页面版本编号 * * 最小的可行页面大小可能是64B,能放下页面的首部、空闲空间,以及一个最小的元组。当然在实践中 * 肯定要大得多(默认为8192B),所以页面大小必须是256的倍数并不是一个重要限制。而在另一端, * 我们最大只能支持32KB的页面,因为 lp_off/lp_len字段都是15bit */ typedef struct PageHeaderData { PageXLogRecPtr pd_lsn; /* 最近应用至本页面XLog记录的LSN */ uint16 pd_checksum; /* 校验和 */ uint16 pd_flags; /* 标记位 */ LocationIndex pd_lower; /* 空闲空间起始位置 */ LocationIndex pd_upper; /* 空闲空间终止位置 */ LocationIndex pd_special; /* 特殊用途空间的开始位置 */ uint16 pd_pagesize_version; TransactionId pd_prune_xid; /* 最老的可修剪XID, 如果没有设置为0 */ ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* 行指针的数组 */ } PageHeaderData; /* 缓冲区页中的项目指针(item pointer),也被称为行指针(line pointer) * * 在某些情况下,项目指针处于 “使用中”的状态,但在本页中没有任何相关联的存储区域 * 按照惯例,lp_len == 0 表示该行指针没有关联存储。独立于其lp_flags的状态 */ typedef struct ItemIdData { unsigned lp_off:15, /* 元组偏移量 (相对页面起始处) */ lp_flags:2, /* 行指针的状态,见下面代码中的几个define */ lp_len:15; /* 元组的长度,以字节计 */ } ItemIdData; /* lp_flags有下列可能的状态,LP_UNUSED的行指针可以立即重用,而其他状态不行。*/ #define LP_UNUSED 0 /* unused (lp_len必须始终为0) */ #define LP_NORMAL 1 /* used (lp_len必须始终>0) */ #define LP_REDIRECT 2 /* HOT 重定向 (lp_len必须为0) */ #define LP_DEAD 3 /* 死元组,有没有对应的存储尚未可知 */
行指针的末尾与最新元组起始位置之间的空余空间称为空闲空间或空洞。
为了识别表中的元组,数据库内部会使用元组标识符(tuple identifier,TID)。TID由一对值组成,分别是元组所属页面的区块号和指向元组的行指针的偏移号。TID 的一种典型用途是索引,更多细节参见第1.4.2节。
结构体PageHeaderData定义于src/include/storage/bufpage.h中。
此外,大小超过约2KB(8KB的四分之一)的堆元组会使用一种称为TOAST(The Oversized-Attribute Storage Technique,超大属性存储技术)的方法来存储与管理。详情请参阅官方文档。