当前位置: 首页 > news >正文

数据库--页(page)

在数据库系统中,页(Page) 是数据存储和管理的最小物理单元,它是磁盘与内存之间数据传输的基本单位。

postgres官网对页(page)的介绍

页的核心定义

  • 物理存储单元:页是数据库在磁盘上分配的一段固定大小的连续存储空间,用于存储表数据、索引、元数据等。
  • 内存操作单元:当数据库读写数据时,以页为单位将数据从磁盘加载到内存(Buffer Pool)或写回磁盘。
  • 统一管理单位:所有增删改查操作最终都转化为对页的修改。

常见页大小

数据库/存储引擎默认页大小可配置性
MySQL InnoDB16 KB支持 (innodb_page_size)
PostgreSQL8 KB编译时固定 (可以配置)
SQL Server8 KB不可配置
Oracle8 KB / 16 KB表空间级别配置

页的内部结构

一个页通常包含以下组成部分(以InnoDB为例):

  1. 页头(Page Header)
    • 存储页的元信息:页类型(数据页、索引页等)、当前记录数、上一页/下一页指针(用于双向链表)。
  2. 数据行(User Records)
    • 实际存储的数据行(Row),按主键顺序或插入顺序排列。
  3. 空闲空间(Free Space)
    • 未使用的存储区域,用于后续插入新数据。
  4. 页目录(Page Directory)
    • 类似目录结构,存储数据行的相对位置(槽/Slot),用于加速页内数据查找(二分查找)。
  5. 页尾(Page Trailer)
    • 校验信息(如Checksum),确保数据完整性。

以下是 PostgreSQL 官方文档中对 页(Page) 的详细描述(基于提到的 Table 65.2. Overall Page Layout),结合技术原理和实际应用场景的解析:

PostgreSQL 页(Page)的物理结构

PostgreSQL 使用 堆表(Heap-Organized Table) 存储数据,所有数据页的物理结构遵循统一布局。以下内容来自官方文档:Database Page Layout。

表 65.2 整体页布局(核心组件)

组件描述
PageHeaderData页头信息(24字节),包含页的元数据。
ItemIdData项标识符数组,每个条目指向页内数据行的位置(4字节/条目)。
Free Space未分配空间,新数据行从尾部插入,新项标识符从头部分配。
Items实际存储的数据行(即表中的行或索引条目)。
Special Space特殊空间,索引访问方法专用(如B-Tree索引存储额外元数据),普通表为空。

1. PageHeaderData(页头)

作用:存储页的全局管理信息。
核心字段

typedef struct PageHeaderData {PageXLogRecPtr pd_lsn;        // 最后一次修改的WAL日志位置(用于崩溃恢复)uint16        pd_checksum;     // 页的校验和(可选,用于数据完整性验证)uint16        pd_flags;        // 标志位(如是否为空、是否需要清理)LocationIndex pd_lower;        // 空闲空间起始位置(ItemIdData数组的结束位置)LocationIndex pd_upper;        // 空闲空间结束位置(Items的起始位置)LocationIndex pd_special;      // 特殊空间的起始位置uint16        pd_pagesize_version; // 页大小和版本号TransactionId pd_prune_xid;    // 最旧的可修剪事务ID(用于MVCC垃圾回收)ItemIdData    pd_linp[FLEXIBLE_ARRAY_MEMBER]; // 项标识符数组(ItemIdData)
} PageHeaderData;
  • 关键意义
    通过 pd_lowerpd_upper 管理空闲空间,确保高效分配和回收。
    pd_prune_xid 支持 MVCC(多版本并发控制)的过期数据清理。

2. ItemIdData(项标识符)

  • 作用:定位页内数据行的物理位置。
  • 结构
    每个 ItemIdData 是一个 (offset, length) 对,占4字节:
    offset:数据行在页内的偏移量(从页头开始的字节数)。
    length:数据行的长度(包括行头和数据内容)。
  • 示例
    若某行的 ItemIdData(100, 64),表示该行从页的第100字节开始,占64字节。

3. Free Space(空闲空间)

  • 动态管理
    插入数据:新数据行从空闲空间的尾部pd_upper)开始分配。
    分配项标识符:新 ItemIdData 从空闲空间的头部pd_lower)分配。
  • 空间回收
    • 删除数据行时,对应的 ItemIdData 被标记为“死亡”,空间可被后续插入重用。
    • 通过 VACUUM 命令可回收碎片化的空闲空间。

4. Items(数据行)

  • 存储内容
    普通表:存储表的数据行(Heap Tuple),包含行头(HeapTupleHeader)和用户数据。
    索引:存储索引条目(如B-Tree的键值和指向堆表行的CTID)。
  • 行头结构(HeapTupleHeader):
    typedef struct HeapTupleHeaderData {uint32        t_xmin;          // 插入事务IDuint32        t_xmax;          // 删除/更新事务IDCommandId     t_cid;           // 命令ID(同一事务内的操作顺序)ItemPointerData t_ctid;        // 当前行版本的CTID(用于行更新)uint16        t_infomask;      // 标志位(如行是否可见、是否有NULL字段)uint8         t_hoff;          // 行头长度bits8         t_bits[FLEXIBLE_ARRAY_MEMBER]; // NULL字段位图
    } HeapTupleHeaderData;
    
  • MVCC支持
    通过 t_xmint_xmax 实现多版本控制,旧版本数据对活跃事务可见,直到被 VACUUM 清理。

5. Special Space(特殊空间)

  • 索引专用
    B-Tree索引:存储索引页的元数据,如右侧子页的指针、页层级等。
    GiST/GIN索引:存储与索引方法相关的扩展信息。
  • 普通表:此区域为空。

页结构对性能的影响

  1. 插入性能
    • 频繁插入导致页内空闲空间减少,可能触发新页分配(非聚簇索引无页分裂)。
    • 使用 fillfactor 预留空间减少更新时的页分裂:

    CREATE TABLE mytable (id INT) WITH (fillfactor = 80); -- 预留20%空间
    
  2. 查询性能
    • 通过索引找到CTID后,需根据页号和偏移量读取堆表页(可能触发随机I/O)。
    • 使用覆盖索引或 CLUSTER 命令优化顺序访问。

  3. 空间效率
    • 小行(如INT类型)可能导致页内空间浪费,大行(如TEXT)触发TOAST机制分页存储。


PostgreSQL页物理结构的总结

页是PostgreSQL物理存储的核心,通过固定结构管理数据行、空闲空间和事务版本。
设计建议
• 合理使用 fillfactor 平衡插入性能和存储效率。
• 定期执行 VACUUMCLUSTER 维护页的空间布局。
扩展阅读
TOAST Storage(大字段分页存储机制)。

页的生命周期(以 PostgreSQL 为例)

在 PostgreSQL 中,页(Page) 是数据存储的核心物理单元,其生命周期从磁盘分配开始,经历数据写入空间管理回收重用,最终可能被释放或覆盖。以下是页的完整生命周期解析:

1. 初始化:页的创建

触发条件

  • 表/索引扩展:当现有页无法容纳新数据时,PostgreSQL 向文件系统申请新的页。
  • 批量导入:通过 COPYINSERT ... SELECT 快速插入大量数据,触发表文件扩展。

初始化操作

  • 格式化为空页:页头(PageHeaderData)初始化,pd_lower 指向项标识符数组起点,pd_upper 指向空闲空间尾部,pd_special 根据页类型(数据页/索引页)设置。
  • 加入空闲空间映射(FSM):记录该页的空闲空间信息,供后续插入使用。

页的变化:

页头(PageHeaderData)
  • pd_lower:初始指向项标识符数组(ItemIdData)的起始位置,此时数组为空,因此 pd_lower 值为 SizeOfPageHeaderData(页头固定大小,通常 24 字节)。
  • pd_upper:初始指向空闲空间的尾部,即页末尾减去 pd_special 的空间(例如,索引页可能有特殊用途区域)。对于普通堆表页,pd_special 为 0,因此 pd_upper 初始值为 BLCKSZ - 1(如 8KB 页为 8192 字节)。
  • pd_flags:标记页类型(如普通数据页、索引页等)及状态(如是否含 HOT 更新)。
项标识符数组(ItemIdData)
  • 初始为空,未分配任何项标识符。
数据区域
  • 完全空闲,无数据行(tuple)。

2. 使用阶段:数据写入与更新

数据插入

  • 分配项标识符:从空闲空间头部(pd_lower)分配 ItemIdData,指向空闲空间尾部(pd_upper)的新数据行。
  • 更新页头pd_lowerpd_upper 动态调整,反映页内空间占用情况。

数据更新

  • MVCC 机制:旧数据行保留在页中,新版本写入其他页(或同一页的空闲空间),原行标记为“死亡”(dead tuple)。
  • HOT(Heap-Only Tuple)优化:若新版本与旧版本在同一页且索引键未修改,直接复用项标识符,避免索引更新。

数据删除

  • 标记死亡行:行头 t_xmax 设置为删除事务ID,空间暂时不可用,需等待 VACUUM 回收。

页的变化

插入数据
  • 项标识符分配:从 pd_lower 处分配一个项标识符(每个占 4 字节),记录数据行的偏移量和长度。每插入一行,pd_lower ​​向页尾方向移动​​(如从 24字节 → 28字节)。
  • 数据行写入:实际数据行从页尾的空闲空间起始位置(pd_upper)开始写入。每插入一行,pd_upper ​​向页头方向移动​​(如从 8192字节 → 8092字节)。(如从 8192 字节 → 8192 - 100 字节 = 8092 字节)。
  • 空闲空间pd_upper - pd_lower 逐渐减小,表示可用空间减少。例如 8092 - 28 = 8064 字节。
更新数据(MVCC)
  • 旧数据行标记:原数据行的行头 t_xmax 设置为删除事务 ID,标记为死亡。
  • 新数据行写入:新版本行写入当前页或其他页的空闲空间,占用新的项标识符和数据区域,pd_lowerpd_upper 继续调整。
删除数据
  • 行头标记:与更新类似,t_xmax 标记为删除事务 ID,空间暂不回收。

3. 维护阶段:空间回收与优化

VACUUM 操作

  1. ​普通 VACUUM:逻辑清理(不移动数据行)​​
  • 清理死亡行​​:
    仅将死亡行的项标识符标记为“未使用”,并更新 pd_lower(移除无效项标识符)。
    ​​不移动存活数据行​​,因此 pd_upper ​​不会改变​​。
  • ​空闲空间合并​​:
    存活数据行仍分散在页中,空闲空间可能碎片化
    后续插入会优先使用空闲区域,但无法保证连续性。
    清理死亡行移除已提交事务不再需要的死亡行,回收空间到空闲区域。
    更新 FSM:向空闲空间映射报告当前页的空闲容量。
  1. VACUUM FULL / CLUSTER:物理重组(移动数据行)​​
  • ​​移动存活数据行​​
    所有存活数据行被 ​​从页尾向页头方向紧凑排列​​消除碎片
    存活数据行的新位置 ​​从页头开始连续存储​​(与插入方向相反)。
  • ​​重置指针​​:
    pd_upper 被重置为 ​​存活数据行的末尾位置​​(即空闲空间起始位置)。
    pd_lower 被重置为 ​​项标识符数组的末尾位置​​。
  • 结果​​:
    空闲空间从 pd_upper 到页尾变为 ​​连续大块​​,后续插入可高效利用

页内空间合并

  • 若空闲空间碎片化,VACUUM FULLCLUSTER 命令会将数据行紧凑排列,重置 pd_lowerpd_upper

索引页分裂(仅索引页)

  • B-Tree 索引页分裂:当索引页无法容纳新键值时,分裂为两个页,旧页保留部分数据,新页分配并插入剩余数据。

页的变化

VACUUM 操作
  • 清理死亡行:扫描页内所有项标识符,移除指向死亡行的项,回收其占用的空间。
  • 合并空闲空间
    ◦ 存活数据行向页头部紧凑排列,pd_upper 重置为存活数据行末尾位置。
    pd_lower 重置为项标识符数组末尾位置(如从 28 字节 → 24 + 4*N 字节,N 为存活项数量)。
  • 更新 FSM:将当前页的空闲空间大小(pd_upper - pd_lower)记录到空闲空间映射。
VACUUM FULL / CLUSTER
  • 页内重组:将数据行完全重新排列,消除碎片,pd_lowerpd_upper 恢复为类似初始化的紧凑状态。
索引页分裂
  • 旧页调整:分裂后,旧页保留部分键值,pd_lowerpd_upper 更新以反映剩余数据。
  • 新页初始化:新分配的页经历初始化阶段,结构与旧页分裂前类似。

矛盾点解析:为什么重组后数据行“向页头紧凑”?

插入方向:数据行从页尾写入,是 为了避免移动已有数据,提高插入效率。
重组方向:数据行从页头紧凑,是 为了消除碎片,保证空闲空间连续。

看似矛盾,实则互补
插入阶段:牺牲空间碎片化换取插入速度。
维护阶段:牺牲重组时间成本换取空间连续性。


示例:页结构变化全流程

假设页大小为 8KB(8192字节),初始状态:
pd_lower = 24(页头固定大小)
pd_upper = 8192(页尾)

步骤1:插入3行(每行100字节)

• 分配项标识符:pd_lower = 24 + 3*4 = 36
• 写入数据行:pd_upper = 8192 - 3*100 = 7892
• 空闲空间:7892 - 36 = 7856 字节(但分布碎片化)。

步骤2:删除1行(假设第二行死亡)

• 项标识符标记为无效,但数据行仍占用空间。
pd_lowerpd_upper 不变。

步骤3:执行 VACUUM FULL

• 移动存活数据行(第1、3行)到页头:
• 第1行写入位置 24 + 2*4 = 32(项标识符结束位置)。
• 第3行写入位置 32 + 100 = 132
• 重置指针:
pd_lower = 32(2个有效项标识符)。
pd_upper = 132 + 100 = 232(存活数据行结束位置)。
• 空闲空间:8192 - 232 = 7960 字节(连续大块)。


总结

插入方向:从页尾向页头写入,避免移动数据,提高插入效率。
重组方向:从页头向页尾紧凑,消除碎片,优化空间连续性。
优化平衡
• 频繁插入/更新的表建议设置 fillfactor < 100 预留空间。
• 定期执行 VACUUM 或自动化 autovacuum 清理碎片。
• 对性能敏感场景谨慎使用 VACUUM FULL(锁表且耗时)。

重新整理后的页在插入新数据时的详细过程

在 PostgreSQL 中,当页经过 VACUUM FULLCLUSTER 维护操作后,其空闲空间被整理为连续的大块,此时插入新数据的过程会更加高效。以下是重新整理后的页在插入新数据时的详细过程:

1. 维护后的页状态

假设页经过重组后,结构如下:
pd_lower:指向项标识符数组(ItemIdData)的末尾(例如 32 字节,表示已分配 2 个项标识符)。
pd_upper:指向存活数据行的末尾(例如 232 字节)。
空闲空间:从 232 字节 到页尾(8192 字节),连续可用空间为 8192 - 232 = 7960 字节

2. 插入新数据的具体流程

(1) 分配项标识符(ItemIdData)
位置:从项标识符数组的末尾(pd_lower)开始分配新的项标识符。
操作
• 新项标识符占 4 字节,插入到 pd_lower 位置。
pd_lower 向页尾方向移动(如从 32 字节 → 36 字节)。
(2) 写入数据行

  • 位置:从空闲空间的起始位置(pd_upper,即 232 字节)开始写入新数据行。
  • 操作
    假设新行大小为 100 字节,数据写入后占据 232 字节332 字节
    pd_upper 向页头方向移动(从 232 字节 → 332 字节)。

(3) 更新页头元数据

  1. 空闲空间
  • 空闲空间从 7960 字节 减少为 7960 - 100 = 7860 字节
  • 新的空闲空间范围:332 字节8192 字节(仍然连续)。

(4) 最终页状态
pd_lower = 36 字节(3 个项标识符)。
pd_upper = 332 字节(存活数据行末尾)。
空闲空间8192 - 332 = 7860 字节


3. 重新整理后的插入优势

与未整理的页相比,重组后的页插入新数据具有以下优势:

  1. 连续空间利用
    • 空闲空间是连续的,新数据行可以直接从 pd_upper 开始写入,无需跳过碎片区域。
  2. 避免页分裂(针对索引页):
    • 对于索引页(如 B-Tree),连续空间减少了因中间插入键值导致的页分裂概率。
  3. 高效 HOT 更新
    • 如果新更新的行与原行在同一页且索引键未修改,可直接复用项标识符,避免索引页更新。

4. 示例场景
场景1:插入小行(100 字节)

步骤

  1. 分配项标识符:pd_lower=32 → 36
  2. 写入数据行:pd_upper=232 → 332
  3. 空闲空间减少,但仍保持连续。
    结果:插入高效,无碎片。
场景2:插入大行(2000 字节)

步骤

  1. 检查空闲空间是否足够(7860 字节 > 2000 字节)。
  2. 分配项标识符:pd_lower=36 → 40
  3. 写入数据行:pd_upper=332 → 2332
    结果:成功插入,空闲空间仍连续(8192 - 2332 = 5860 字节)。

5. 维护后的潜在问题与优化

(1) 问题:预留空间耗尽

现象:若插入大量数据导致 pd_upper 接近 pd_lower,页将再次填满。
优化
• 设置 fillfactor 预留空间(如 fillfactor=80),仅填充页的 80%,剩余 20% 用于更新和插入。
sql CREATE TABLE mytable (id INT) WITH (fillfactor = 80);

(2) 问题:随机插入导致碎片再生

现象:若主键非自增,新数据可能插入到页中间(如索引页),破坏连续性。
优化
• 对频繁更新的表使用 自增主键时序字段,使插入集中在页尾部。
• 定期执行 CLUSTER 按主键顺序重组数据。


6. 与未整理页的对比

场景未整理的页整理后的页
空闲空间碎片化(多个不连续区域)连续大块
插入速度可能需遍历碎片区域直接分配尾部空间,速度更快
更新优化(HOT)空间碎片可能导致跨页更新紧凑空间更易触发 HOT 更新
索引页分裂概率较高(随机插入导致中间分裂)较低(顺序插入占优)

总结

  • 重新整理后的插入过程
    项标识符从页头分配,数据行从页尾写入,空闲空间连续,插入高效。
  • 优化建议
    • 对高频写入表设置 fillfactor 预留空间。
    • 优先使用顺序插入(如自增主键)减少碎片。
    • 定期维护(VACUUMCLUSTER)保持页结构紧凑。
  • 维护的价值
    将碎片化空间整理为连续块,显著提升插入效率,降低页分裂和随机 I/O 的开销。

4. 重用阶段:空闲页的再分配

同一对象重用
  • 若页被完全清空(如所有行被删除),该页被标记为“空闲”,优先用于同一表或索引的后续插入。
跨对象重用
  • 空闲页可被其他表或索引通过全局空闲空间池(Free Space Map)分配使用,减少磁盘空间申请开销。
页的变化
同一对象重用
  • 插入新数据时,优先使用已回收的空闲页。pd_lowerpd_upper 根据新数据插入动态调整,类似于使用阶段。
跨对象重用
  • 页被重新分配给其他表或索引时,需重新初始化(格式化页头、重置 pd_lowerpd_upper),并更新 FSM。

5. 释放阶段:页的终结

文件截断(Truncate)
  • 执行 TRUNCATE TABLE 时,表文件末尾的空闲页可能被释放回操作系统(需满足 wal_level=minimal 且无并发事务)。
物理删除
  • 表或索引被 DROP 时,所有关联页最终由文件系统回收。
页的变化
文件截断(TRUNCATE)
  • 表末尾的空闲页被释放,文件大小缩减,操作系统回收物理空间。这些页的元数据(如页头)被清除。
物理删除(DROP)
  • 页所属文件被删除,所有关联页的磁盘空间由文件系统回收。

生命周期中的关键影响

  1. 性能瓶颈
    • 频繁更新/删除:导致死亡行堆积,需频繁 VACUUM 避免空间膨胀。
    • 随机插入:非自增主键插入可能导致页分裂(索引页)或空间碎片化(堆表页)。
  2. 优化策略
    • 定期维护:通过 VACUUMANALYZE 保持页内空间高效。
    • 合理设计表结构:使用 fillfactor 预留更新空间,避免小字段浪费页容量。
    • 使用 HOT 更新:减少索引页修改频率。

示例:页生命周期全流程

  1. 新建表 users
    • 初始为 0 页,插入第一条数据时分配第 1 页(初始化)。
  2. 持续插入数据
    • 页 1 填满后分配页 2,依此类推。
  3. 更新 users
    • 页 1 中某行被更新,旧数据标记为死亡,新数据写入页 2(HOT 更新可能复用页 1)。
  4. 执行 VACUUM
    • 清理页 1 的死亡行,空闲空间可用于新插入。
  5. 删除所有数据
    • 页 1 和页 2 被标记为空闲,后续插入可能重用这些页。
  6. 删除表 users
    • 所有关联页被标记为可释放,最终由文件系统回收。

总结

PostgreSQL 的页生命周期围绕 空间分配 → 数据操作 → 维护回收 → 释放 展开,核心目标是高效管理磁盘与内存资源。理解这一过程有助于优化数据存储设计、制定维护策略(如自动化 VACUUM),并诊断空间膨胀或写入性能问题。

pd_lower​​不是应该挨着存活数据行的头部吗,32字节变36字节,这个时候不应该是存活数据行占用着33到232字节吗

提到的困惑源于对 项标识符(ItemIdData)数据行(Items) 存储位置的混淆。让我们通过一个具体示例和图示彻底理清这一机制:

1. 页结构的核心规则

在 PostgreSQL 中,页的存储遵循以下规则:

  1. 项标识符数组(ItemIdData)
    • 从页头(PageHeaderData 结束位置)开始分配,向页尾方向增长
    • 每个项标识符占 4 字节,记录数据行的 偏移量(offset)长度(length)
  2. 数据行(Items)
    • 从页尾开始分配,向页头方向增长
    • 数据行的实际存储位置由项标识符中的 offset 决定。

2. 维护后的页结构(重组后)

假设页经过 VACUUM FULL 重组后,状态如下:
页头(PageHeaderData):24 字节。
项标识符数组(ItemIdData):2 个有效项,占 8 字节(24 → 32 字节)。
数据行(Items)
• 第 1 行:偏移量 200,长度 100(占用 200 → 300 字节)。
• 第 2 行:偏移量 300,长度 100(占用 300 → 400 字节)。
指针位置
pd_lower = 32(项标识符数组末尾)。
pd_upper = 400(数据行末尾)。
空闲空间400 → 8192 字节(连续大块)。

3. 插入新数据的过程

(1) 分配项标识符

位置:从 pd_lower = 32 分配新项标识符(4 字节)。
结果
• 项标识符数组扩展为 24 → 36 字节
pd_lower 更新为 36

(2) 写入数据行

位置:从 pd_upper = 400 开始写入新数据行(假设长度 100 字节)。
结果
• 数据行占用 400 → 500 字节
pd_upper 更新为 500

(3) 更新项标识符

新项标识符内容(offset=400, length=100)
作用:通过该标识符可定位新数据行的位置。

(4) 最终页结构
  • 项标识符数组:3 个项,占 24 → 36 字节
  • 数据行
    • 第 1 行:200 → 300
    • 第 2 行:300 → 400
    • 第 3 行:400 → 500
  • 指针位置
    pd_lower = 36
    pd_upper = 500
  • 空闲空间500 → 8192 字节(仍连续)。

4. 关键澄清:为什么数据行不紧挨项标识符数组?

  • 项标识符是索引,而非数据行存储位置
    项标识符数组仅存储指向数据行的元数据(offset 和 length),实际数据行始终从页尾向页头写入。即使经过 VACUUM FULL 重组,数据行仍按偏移量分布在页中,而非紧邻项标识符数组。

  • 示例中的矛盾点解析
    你提到的「存活数据行占据 33 到 232 字节」是误解。实际数据行的位置由项标识符中的 offset 决定,可能分布在页的任意位置(如示例中的 200 → 500 字节),与项标识符数组(24 → 36 字节)无直接相邻关系。


5. 为何重组后数据行看似“向页头紧凑”?

  • 重组的目标是消除碎片,而非改变写入方向
    VACUUM FULL 会将所有存活数据行的 偏移量(offset) 调整为从页尾开始连续排列,但写入方向仍保持 从页尾向页头。重组后,空闲空间集中在页尾,新插入的数据依然从 pd_upper(重组后的数据行末尾)开始写入。
重组前后的对比
操作数据行分布空闲空间
重组前(碎片化)分散在 200-300, 500-600300-500, 600-8192
重组后(紧凑)连续在 200-400400-8192(连续大块)

6. 总结

  • 项标识符和数据行是分离的
    项标识符数组在页头附近增长,数据行在页尾附近增长,两者通过 offset 关联,物理位置无直接相邻关系。
  • 重组不改变写入方向
    重组仅合并空闲空间,数据行仍从页尾写入,但偏移量连续排列以消除碎片。
  • 性能优化本质
    通过整理碎片,确保后续插入能利用连续大块空间,减少随机 I/O 和页分裂。

理解这一机制后,可以更精准地设计表结构(如选择自增主键)和制定维护策略(如定期 VACUUM),从而提升数据库性能。

页的作用与意义

(1) 高效磁盘I/O
  • 按页加载:数据库读取数据时,即使只需一行数据,也会将整个页加载到内存。
    示例
    若页大小为16 KB,查询一行数据(假设100字节)时,实际读取16 KB的数据到内存。后续访问同一页内的其他数据时,无需磁盘I/O
(2) 数据组织与管理
  • 数据行存储:页内的数据行按主键顺序(聚簇索引)插入顺序(堆表)排列
  • 空间复用:删除数据时,页内空间被标记为可重用,避免频繁申请新页。
(3) 索引实现的基础
  • B+树节点即页索引的每个节点(根节点、分支节点、叶子节点)对应一个页
  • 示例
    InnoDB的B+树索引中:
    • 叶子节点页:存储实际数据行(聚簇索引)或主键值+指针(二级索引)。
    • 非叶子节点页:存储索引键值范围和指向子页的指针。

4. 页与性能优化

(1) 页分裂(Page Split)
  • 触发条件:当向已满的页插入新数据时(尤其在聚簇索引中),数据库会将页分裂为两个新页。
  • 性能影响
    • 增加I/O操作和锁竞争,降低写入性能。
    • 导致页填充率下降(例如从100%降至约50%),产生存储碎片。
  • 优化方法
    • 使用自增主键(顺序插入)减少页分裂。
    • 定期执行 OPTIMIZE TABLE(MySQL)或 VACUUM FULL(PostgreSQL)重组页。
(2) 页合并(Page Merge)
  • 触发条件:当相邻页的空间利用率较低时,数据库可能合并这些页以释放空闲空间。
  • 优化建议
    • 避免频繁的删除操作,或定期维护表空间。
(3) 内存缓存(Buffer Pool)
  • 缓存页:数据库将热点数据页缓存在内存的Buffer Pool中,减少磁盘I/O。
  • 优化参数
    • MySQL的 innodb_buffer_pool_size:建议设置为物理内存的70%~80%。
    • PostgreSQL的 shared_buffers:建议设置为物理内存的25%~40%。

5. 不同数据库的页实现差异

(1) MySQL InnoDB
  • 页类型多样化
    • 数据页(INDEX)、Undo页(UNDO_LOG)、系统页(SYS)等。
  • 支持压缩页:通过 KEY_BLOCK_SIZE 压缩表数据,减少存储占用(例如压缩为8 KB或4 KB)。
(2) PostgreSQL
  • 堆表(Heap Table):数据页不强制按主键顺序存储,依赖额外的索引(如B-Tree)加速查询。
  • TOAST机制:针对大字段(如TEXT、JSONB),自动将数据拆分到多个TOAST页中存储。

6. 页的直观比喻

将数据库表比作一本书:
页(Page) = 书的一页纸。
行(Row) = 页中的一行文字。
索引(Index) = 书的目录,通过目录(索引)快速定位到某一页(页指针)。


总结

页是数据库存储的原子单位,决定了数据物理存储和内存管理的效率。
• 理解页的结构和机制,有助于优化表设计(如主键选择、字段类型)、诊断性能问题(如页分裂、I/O瓶颈)和配置数据库参数。

页分裂(Page Split)

定义与原理

在数据库系统中,​​页分裂(Page Split)​​ 是一种物理存储重组机制,主要发生在 ​​索引结构(如B+树)​​ 中。当向已满的页插入新数据时,数据库会将其分裂为两个新页,以容纳更多数据

页分裂的触发条件​

  • ​​索引页已满​​:当插入新键值(Key)时,目标页的剩余空间不足以容纳新数据。
  • 非顺序插入​​:如果插入的键值不在当前页的最大/最小值范围内(如随机主键插入),可能导致中间位置插入,触发分裂。

发生场景

插入新数据:当数据必须插入到已满的页中(如非自增主键的随机插入)。
更新数据:数据更新后长度增加,导致页空间不足。

具体过程(以B+树索引为例)

假设有一个B+树索引页(页A),已存储键值 [10, 20, 30, 40],页容量为4个键值。当插入新键值 25 时触发分裂:

  1. 触发条件
    向页A插入新数据,但页A剩余空间不足。
  2. 确定分裂点
    数据库根据算法确定分裂点(如中间位置)。
    ​​示例​​:将键值 30 作为分裂点,左侧保留 [10, 20],右侧包含 [25, 30, 40]。
  3. 分裂操作
    • 创建新页B,将分裂点右侧的键值(25, 30, 40)迁移到页B。。
    • 原页A保留左侧键值(10,20) ,将页A的部分数据迁移到页B,并重新分配数据,使新旧页均满足填充因子(Fill Factor)要求。
    • 更新索引结构,维护页之间的双向链表指针。
  4. 更新父页指针
    将分裂点键值(30)提升到父页,建立页A和页B的父子关系
    示例:父业新增条目30,指向页B
  5. 插入新数据
    新建值25插入页B的合适位置(最终页B包含25,30,40)

示例对比

自增主键:新数据按顺序插入页末尾,减少分裂概率。

CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY,  -- 自增主键name VARCHAR(50)
);

随机主键(如UUID):新数据随机插入,频繁触发页中间分裂。

CREATE TABLE users (id CHAR(36) PRIMARY KEY,  -- UUID主键name VARCHAR(50)
);

页分裂的性能影响​

  • 写入延迟:分裂涉及磁盘I/O(分配新页、迁移数据、更新父页)和锁竞争,导致插入性能下降。

  • 空间碎片:分裂后,新页的填充率通常为50%~70%(例如原页100%满,分裂后各页50%),浪费存储空间。

  • 索引树高度增加:若父页也因分裂而满,可能递归触发分裂,导致树层级加深,查询路径变长。

不同数据库的页分裂差异

  1. MySQL InnoDB(聚簇索引
  • 聚簇索引:数据按主键顺序存储在叶子节点页中。
    • 顺序插入(如自增主键):新数据追加到页尾部,分裂概率低。
    • 随机插入(如UUID主键):频繁中间插入,触发分裂。
    • 优化建议:

    CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY,  -- 自增主键减少分裂name VARCHAR(50)
    );
    
  1. PostgreSQL(堆表+索引)
  • 堆表数据无序:数据页本身不按主键顺序存储,插入新行时不会触发数据页分裂。

  • 索引页分裂:B-Tree索引的页分裂逻辑与MySQL类似,但索引条目指向堆表行的CTID(物理地址)。

  • 优化建议:

    CREATE INDEX idx_name ON users(name) WITH (fillfactor=90);  -- 预留10%空间
    

页分裂的监控与诊断

(1) 监控指标
• MySQL:

SHOW ENGINE INNODB STATUS\G  -- 查看 "INSERT BUFFER AND ADAPTIVE HASH INDEX" 中的分裂计数

• PostgreSQL:

SELECT * FROM pg_stat_user_indexes;  -- 观察索引扫描和更新频率

(2) 日志分析
• 启用数据库的页分裂日志(如MySQL的InnoDB监控日志),定位高频分裂的索引。

如何避免或减少页分裂

  1. 合理设计主键
    • 使用自增主键:确保数据顺序插入,减少中间分裂。
    • 避免随机主键:如UUID或哈希值,改用有序值(如雪花算法ID)。

  2. 调整填充因子(Fill Factor)
    • 预留空间:设置索引页的填充因子(如90%),预留空间给未来更新。

-- PostgreSQL
CREATE INDEX idx_orders ON orders(order_date) WITH (fillfactor=80);-- MySQL(通过重建表生效)
ALTER TABLE orders KEY_BLOCK_SIZE=8;
  1. 定期维护
    • 重建索引:消除碎片,恢复高填充率。
-- PostgreSQL
REINDEX INDEX idx_orders;-- MySQL
OPTIMIZE TABLE orders;
  1. 分区与分表
    • 水平分片:将数据分散到多个物理表中,减少单个索引的写入压力。

页分裂 vs. 页合并

特性页分裂(Page Split)页合并(Page Merge)
触发条件页空间不足时插入新数据相邻页空间利用率低时删除数据
操作代价高(需分配新页、迁移数据、更新父页)中(合并相邻页,更新指针)
优化目标解决空间不足问题减少空间碎片,提高存储利用率
频率高频写入场景下常见低频(依赖删除操作和后台维护)

页分裂总结

  • 优化核心:通过顺序插入、预留空间、定期维护等手段减少分裂频率。

  • 选择策略:

    • OLTP场景(高并发写入):优先使用自增主键和填充因子。

    • OLAP场景(批量加载):关注索引重建和存储参数调优。

MySQL vs PostgreSQL的差异

特性MySQL (InnoDB)PostgreSQL
回表机制二级索引存储主键值,回表查聚簇索引。所有索引存储CTID(物理地址),直接定位堆表数据。
页分裂场景仅聚簇索引会触发页分裂。堆表数据无序,插入新数据不会导致页分裂,但索引更新可能触发索引页分裂。
优化手段使用自增主键减少分裂。定期执行 CLUSTER 命令重组表数据,或调整填充因子(fillfactor)。

如何避免回表和页分裂?

  1. 回表优化
    • 使用覆盖索引(Covering Index)。
    • 避免 SELECT *,只查询必要字段。

  2. 页分裂优化
    MySQL:使用自增主键,避免随机插入。
    PostgreSQL:定期执行 CLUSTER 命令或使用 fillfactor 预留页空间。

    CREATE INDEX idx_name ON users(name) WITH (fillfactor=90);  -- 预留10%空间
    

总结

回表是二级索引查询的额外步骤,可通过覆盖索引优化。
页分裂是聚簇索引写入的物理重组操作,合理设计主键可显著降低其频率。
• MySQL和PostgreSQL在实现细节上有本质差异,需根据数据库类型选择优化策略。

相关文章:

  • UniOcc:自动驾驶占用预测和预报的统一基准
  • CPP_类和对象
  • 智能外呼系统的技术演进与多场景落地实践
  • 【k8s】LVS/IPVS的三种模式:NAT、DR、TUN
  • NOIP2009提高组.Hankson的趣味题
  • Spring JDBC 的开发步骤(非注解方式)
  • SpringBoot入门实战(第七篇:项目接口-商品管理)
  • Ubuntu启动SMB(Samba)服务步骤
  • pytest心得体会
  • vue2+Vant 定制主题
  • 第二章:ForgeAgent Core
  • 极狐GitLab 的合并请求部件能干什么?
  • 【C语言】C语言中的字符函数和字符串函数全解析
  • COMSOL多孔结构传热模拟
  • VTK-8.2.0源码编译(Cmake+VS2022+Qt5.12.12)
  • 零跑B01上海车展全球首秀,定义纯电轿车新基准
  • 3D模型格式转换工具HOOPS Exchange 2025.3.0更新:iOS实现Rhino格式支持!
  • CS144 Lab3 实战记录:TCP 发送器实现
  • 奶茶店里面的数据结构
  • ProxySQL实现mysql8主从同步读写分离
  • 两部门发布“五一”假期全国森林草原火险形势预测
  • 车展之战:国产狂飙、外资反扑、智驾变辅助
  • 八成盈利,2024年沪市主板公司实现净利润4.35万亿元
  • 国家网信办举办在欧中资企业座谈会,就数据跨境流动等进行交流
  • 迪卡侬回应出售中国业务30%股份传闻:始终扎根中国长期发展
  • 新型算法助力听障人士听得更清晰