在数据文件(堆表,索引,也包括空闲空间映射和可见性映射)内部,它被划分为固定长度的页(pages) ,或曰 区块(blocks) ,大小默认为8192字节(8KB)。 每个文件中的页从0开始按顺序编号,这些数字称为区块号(block numbers) 。 如果文件已填满,PostgreSQL通过在文件末尾追加一个新的空页来增长文件。
页面内部的布局取决于数据文件的类型。本节会描述表的页面布局,因为理解接下来的几章需要这些知识。
图 1.4. 堆表文件的页面布局
表的页面包含了三种类型的数据:
- 堆元组(heap tuples) —— 堆元组就是数据记录本身。它们从页面底部开始依序堆叠。 5.2 元组结构 与 第九章 预写式日志——WAL 会描述元组的内部结构,这一知识对于理解PostgreSQL并发控制与WAL机制是必须的。
- 行指针(line pointer) —— 每个行指针占4个字节,保存着指向堆元组的指针。它们也被称为项目指针(item pointer) 。行指针简单地组织为一个数组,扮演了元组索引的角色。每个索引项从1开始依次编号,称为偏移号(offset number) 。当向页面中添加新元组时,一个相应的新行指针也会被放入数组中,并指向新添加的元组。
- 首部数据(header data) —— 页面的起始位置分配了由结构
PageHeaderData
定义的首部数据。它的大小为24个字节,包含关于页面的元数据。该结构的主要成员变量为:pd_lsn
—— 本页面最近一次变更所写入XLOG记录对应的LSN。它是一个8字节无符号整数,与WAL机制相关, 第九章 预写式日志——WAL 将详细展开。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位用于页面版本编号。 * * 最小的可行页面大小可能是64字节,能放下页的首部,空闲空间,以及一个最小的元组。当然在实践中 * 肯定要大得多(默认为8192字节),所以页面大小必需是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, /* 行指针的状态,见下 */ 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 /* 死元组,有没有对应的存储尚未可知 */
行指针的末尾与最新元组起始位置之间的空余空间称为空闲空间(free space) 或空洞(hole) 。
为了识别表中的元组,数据库内部会使用元组标识符(tuple identifier, TID) 。TID由一对值组成:元组所属页面的区块号 ,及指向元组的行指针的偏移号 。TID的一种典型用途是索引,更多细节参见 1.4.2 读取堆元。
结构体
PageHeaderData
定义于src/include/storage/bufpage.h
中。
此外,大小超过约2KB(8KB的四分之一)的堆元组会使用一种称为 TOAST(The Oversized-Attribute Storage Technique,超大属性存储技术) 的方法来存储与管理。详情请参阅PostgreSQL文档。