━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PostgreSQL 页剪枝(Page Pruning)与 HOT 更新
背景:PostgreSQL 的 MVCC 机制
PostgreSQL 使用 MVCC(多版本并发控制)实现事务隔离。每次 UPDATE 不是原地修改,而是:
- 将旧行标记为"已删除"(设置 xmax)
- 插入一条新行(新的物理 tuple)
这意味着频繁更新会产生大量"死元组"(dead tuples),需要 VACUUM 清理。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
HOT 更新(Heap Only Tuple Update)
是什么
HOT 是一种优化机制,允许在同一数据页内 完成更新,且不需要更新索引。
触发条件(必须同时满足)
- 新旧版本在同一个堆页(same heap page)
- 被更新的列不属于任何索引的键列
实现原理
页内布局(更新前):
Index Entry → ItemId[1] → Tuple_v1 (xmax=0)
更新后(HOT):
Index Entry → ItemId[1] → Tuple_v1 (xmax=txid, HOT标志)
↓ (t_ctid 指向同页新版本)
ItemId[2] → Tuple_v2 (xmin=txid, xmax=0)
- 旧 tuple 的 t_ctid 不再指向自身,而是指向同页的新 tuple
- 新 tuple 头部设置 HEAP_ONLY_TUPLE 标志
- 旧 tuple 设置 HEAP_HOT_UPDATED 标志
- 索引不变,仍指向 ItemId[1]
读取时的链式追踪
当通过索引访问时,PostgreSQL 发现 ItemId[1] 指向的 tuple 有 HOT 链,会沿着 t_ctid 链向前追踪,找到对当前事务可见的最新版本。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
页剪枝(Page Pruning)
是什么
页剪枝是一种轻量级的页内清理,在访问某个页时顺带执行,不需要等待 VACUUM。
触发时机
- 当页面空间不足以插入新 tuple 时
- 由 heap_page_prune_opt() 触发(在 heapam.c 中)
做了什么
- 清理 HOT 链中的死元组:如果 HOT 链头部的旧版本对所有活跃事务都不可见,则可以回收
- 重定向 ItemId:将死掉的 ItemId 标记为 LP_REDIRECT,直接指向链中仍存活的版本,缩短追踪路径
- 释放 ItemId 槽位:完全死掉的 tuple 的 ItemId 标记为 LP_DEAD,空间可被复用
- 整理页内碎片(可选,通过 heap_page_prune + PageRepairFragmentation)
与 VACUUM 的区别
| 页剪枝 | VACUUM | |
|---|---|---|
| 触发方式 | 访问页时自动触发 | 后台进程或手动 |
| 范围 | 单个页 | 整张表 |
| 索引清理 | 不处理 | 处理 |
| 事务 ID 回绕保护 | 不处理 | 处理 |
| 开销 | 极低 | 较高 |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
两者的关系
UPDATE 操作
│
├─ 满足 HOT 条件?
│ │
│ YES → HOT 更新(同页,不更新索引)
│ │ │
│ │ └─ 产生页内 HOT 链
│ │ │
│ │ └─ 页剪枝 → 清理链中死元组,回收空间
│ │
│ NO → 普通更新(可能跨页,更新索引)
│ │
│ └─ 需要 VACUUM 清理
HOT 更新减少了索引膨胀,页剪枝则让 HOT 链产生的死元组能被快速回收,两者配合使得高频更新场景下性能大幅提升,减少对 VACUUM 的依赖。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
关键源码位置(PostgreSQL src)
- src/backend/access/heap/heapam.c --- HOT 更新逻辑(heap_update)
- src/backend/access/heap/pruneheap.c --- 页剪枝核心逻辑
- src/include/storage/bufpage.h --- ItemId 标志定义
- src/include/access/htup_details.h --- HEAP_HOT_UPDATED、HEAP_ONLY_TUPLE 标志