1.3 堆表文件的内部布局

在数据文件(堆表,索引,也包括空闲空间映射和可见性映射)内部,它被划分为固定长度的页(pages) ,或曰 区块(blocks) ,大小默认为8192字节(8KB)。 每个文件中的页从0开始按顺序编号,这些数字称为区块号(block numbers) 。 如果文件已填满,PostgreSQL通过在文件末尾追加一个新的空页来增长文件。

页面内部的布局取决于数据文件的类型。本节会描述表的页面布局,因为理解接下来的几章需要这些知识。

图 1.4. 堆表文件的页面布局图 1.4. 堆表文件的页面布局

表的页面包含了三种类型的数据:

  1. 堆元组(heap tuples) —— 堆元组就是数据记录本身。它们从页面底部开始依序堆叠。 5.2 元组结构第九章 预写式日志——WAL 会描述元组的内部结构,这一知识对于理解PostgreSQL并发控制与WAL机制是必须的。
  2. 行指针(line pointer) —— 每个行指针占4个字节,保存着指向堆元组的指针。它们也被称为项目指针(item pointer) 。行指针简单地组织为一个数组,扮演了元组索引的角色。每个索引项从1开始依次编号,称为偏移号(offset number) 。当向页面中添加新元组时,一个相应的新行指针也会被放入数组中,并指向新添加的元组。
  3. 首部数据(header data) —— 页面的起始位置分配了由结构PageHeaderData定义的首部数据。它的大小为24个字节,包含关于页面的元数据。该结构的主要成员变量为:
    • pd_lsn —— 本页面最近一次变更所写入XLOG记录对应的LSN。它是一个8字节无符号整数,与WAL机制相关, 第九章 预写式日志——WAL 将详细展开。
    • pd_checksum —— 本页面的校验和值。(注意只有在9.3或更高版本才有此变量,早期版中该字段用于存储页面的时间线标识)
    • pd_lowerpd_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文档