第 28 章 除旧布新:VACUUM、冻结与膨胀治理
PostgreSQL 的写入并不在提交时把旧世界擦掉。
UPDATE 写出新行版本,DELETE 标记旧版本失效;旧版本必须继续存在,直到所有可能看见
它的快照都离开。这个选择换来了读写并发,也把空间、统计、事务年龄和索引维护变成一套
持续运行的生命周期:
write
-> create obsolete row versions
-> wait until no snapshot can see them
-> prune / VACUUM
-> make page space reusable
-> update FSM / VM / statistics
-> freeze old XID / MXID这条链上任一环被阻断,表现都可能是“膨胀”,但处理方法完全不同:
autovacuum 尚未触发 -> 触发参数与变化速率
worker 已触发但排队 -> worker / I/O / cost budget
VACUUM 扫过却没清掉 -> 长快照、槽、预备事务
页内空间已经可重用 -> 不一定需要缩小文件
数据稳定但 XID 很老 -> aggressive vacuum / freeze
heap 正常但 index 膨胀 -> index diagnosis / reindex
过期数据天然按时间成片 -> detach partition,不做海量 DELETE所以本章不把 VACUUM 当作一个“清垃圾命令”,而把它放回四条相互关联的控制回路:
| 控制回路 | 主要问题 | 关键证据 |
|---|---|---|
| MVCC 回收 | 哪些旧版本已经无人可见 | backend_xmin、pgstattuple、dead tuple |
| 空间与访问 | 空间能否复用,VM/FSM 是否更新 | relation size、FSM、VM、HOT |
| 事务年龄 | 离回卷保护线还有多远 | relfrozenxid、datfrozenxid、relminmxid |
| 结构与生命周期 | 是否需要重建或整片退役 | amcheck、pg_index、partition manifest |
三个必须分开的结论
“不可见”不等于“已经移除”
一条旧版本对当前事务不可见,不代表它对所有事务不可见。决定是否可回收的是全局清理 边界,而不是当前查询的快照。
“已经回收”不等于“文件已经缩小”
普通 VACUUM 主要把空间登记为关系内部可复用。它在特定条件下可能截断关系尾部,但
不承诺把散落在文件中间的空洞交还操作系统。VACUUM FULL 会重写表并取得
ACCESS EXCLUSIVE 锁,不能作为日常扫尾。
“dead tuple 很多”不等于“表已经异常膨胀”
n_dead_tup 是累计统计系统的估计;一次写入尖峰、健康的稳态 churn、被长事务阻断、
真正的 heap bloat,可能给出相似的瞬时数字。至少要联合:
change rate
last vacuum / autovacuum
dead tuple estimate and physical sample
relation growth
free space
oldest xmin holders
workload reuse behavior再决定“等下一轮、手工 VACUUM、解除保留者、在线重建,还是安排离线重写”。
本章实验:让旧快照亲自阻断回收
正式实验在已确认的 Pigsty pg-test 沙箱创建一次性数据库:
database pg36_maintenance
role dbuser_pg36maint
PostgreSQL 18.4
data synthetic only
risk L2 bounded disposable fixture夹具初始有 60,000 行。实验先开启一个 REPEATABLE READ 事务并确认它持有非空
backend_xmin,然后:
UPDATE 40,000 rows
DELETE 10,000 rows
VACUUM while old snapshot remains结果:
| 时点 | 当前行 | pgstattuple dead tuple | heap bytes | FSM 可用空间 |
|---|---|---|---|---|
| 初始 | 60,000 | 0 | 61,440,000 | 19,680,000 |
| churn 后 | 50,000 | 50,000 | 87,040,000 | 27,877,376 |
| 旧快照仍在,普通 VACUUM 后 | 50,000 | 50,000 | 87,040,000 | 17,480,000 |
精确释放旧快照并 VACUUM FREEZE 后 | 50,000 | 0 | 87,040,000 | 51,920,000 |
这个结果同时证明两件事:
- 扫过并不等于能清;旧快照仍需要那些版本时,普通
VACUUM必须保留它们; - 回收并不等于缩文件;最后 dead tuple 为零、FSM 可用约 51.9 MB,heap 文件仍是 87.04 MB。
最后一次维护还得到:
all-visible pages 10,625
all-frozen pages 10,625
relfrozenxid age 14 -> 2注意:这不是在声称普通 VACUUM 永远不会截断文件。本夹具只观察到“文件未缩、空间
可复用”;生产结论必须保留普通 vacuum 有条件截断尾部的例外。
同一条证据链中的索引与分区
实验没有在 heap 回收后停止。
完整性与重建
bt_index_check(... heapallindexed=true, checkunique=true) pass
bt_index_parent_check(... rootdescend=true, ...) pass
REINDEX INDEX CONCURRENTLY pass
invalid index after 0
_ccnew / _ccold artifact after 0被重建的二级索引从 3,227,648 bytes 降到 1,589,248 bytes;relfilenode 改变,
索引仍只有一个、indisready/indisvalid/indislive 全为真。这个数据只说明夹具中的
重建完成,不能外推生产窗口的耗时、锁等待或空间余量。
分区退役
10,000 行过期分区采用:
logical manifest
-> DETACH PARTITION CONCURRENTLY
-> CSV export + SHA-256
-> independent restore table
-> row count / range / sum / digest equality
-> drop detached partition导出文件为 547,894 bytes。只有在回灌后的 10,000 行逻辑摘要完全一致后,实验才删除 独立分区;父表保留 5,000 行当前数据。
公开结果见 maintenance-run.json,安全边界见
lab-contract.md。
本章学习成果
完成本章后,你应该能:
- 从 MVCC 快照解释
UPDATE、DELETE为什么留下旧版本; - 区分 tuple visibility、deadness、removability 与 reusable space;
- 解释 page pruning、HOT、regular vacuum、aggressive vacuum 的分工;
- 用 FSM、VM、relation size 和 physical tuple evidence 分别回答不同问题;
- 正确计算 PostgreSQL 18 的 autovacuum update/delete 与 insert 触发阈值;
- 用表级 storage parameter 做定点治理,同时避免把关闭 autovacuum 当调优;
- 从 worker、cost delay、memory、I/O 和 workload 联合判断维护竞争;
- 读取
pg_stat_progress_vacuum,但不把块比例冒充 ETA; - 从
backend_xmin、复制槽xmin/catalog_xmin、两阶段事务找出保留者; - 监控
relfrozenxid/datfrozenxid和relminmxid,避免只看数据库总年龄; - 在回卷紧急态按安装版本的官方流程解除保留者并让普通
VACUUM完成; - 区分 stable-state bloat、transient churn、index bloat 和 statistics error;
- 评估
VACUUM FULL、在线重写和并发索引重建的锁、空间、WAL 与失败残留; - 用
DETACH ... CONCURRENTLY、归档清单和回灌验证完成分区退役; - 用
amcheck建立分层完整性检查,而不把它当页校验或恢复演练的替代品; - 把 Pigsty 历史指标、日志和 dashboard 与 PostgreSQL 原生视图交叉验证;
- 输出日常、每周、每月和事件驱动的维护清单;
- 在过载与疑似损坏时分别安全路由到第 34、35 章。
本章目录
28.1 死元组与可见性
28.2 autovacuum 的触发与资源
28.3 冻结、XID 与保留者
- 28.3.1 XID 年龄、冻结与回卷保护
- 28.3.2 长事务、
backend_xmin与 idle in transaction - 28.3.3 复制槽
xmin与孤儿pg_prepared_xacts - 28.3.4 紧急态先解除保留并让 VACUUM 完成
28.4 膨胀与重建
28.5 分区生命周期
- 28.5.1 新分区预建、约束和父表显式 ANALYZE
- 28.5.2
DETACH、归档、验证后删除 - 28.5.3 用分区退役替代大批量
DELETE - 28.5.4 将 ch04/ch07/ch11/ch16/ch28 串成能力索引
28.6 amcheck 与例行完整性检查
28.7 实战:建立维护节奏
阅读路线
应用开发者:
28.1 -> 28.2.1 -> 28.3.2 -> 28.5 -> 28.7重点是事务生命周期、长事务边界、表级写入特征和分区保留策略。
平台工程师:
28.1 -> 28.2 -> 28.3 -> 28.4 -> 28.6 -> 28.7重点是触发、资源、回卷安全、重建窗口和完整性检查。
两条路线必须合流:应用定义事务与数据生命周期,平台维护全局清理边界和资源预算。 应用留下无限事务,平台无法“调快 VACUUM”;平台盲目重写,应用也无法获得可预测服务。
版本与证据权威
本章命令以 PostgreSQL 18 为基线,正式 run 使用 18.4;平台示例以 Pigsty 4.4 为 参考实现。维护与紧急恢复语义应按实际安装 major/minor 的 PostgreSQL 官方文档 执行,不把旧版本博客或平台二次说明覆盖到新版本。
特别是事务 ID 即将耗尽的处置,PostgreSQL 18 官方流程要求先处理 prepared xact、
长事务和旧复制槽,再运行普通 VACUUM;它明确不建议在该状态使用
VACUUM FULL 或 VACUUM FREEZE,一般也不需要 single-user mode。本章 28.3.4
按这个版本事实展开。
核心资料:
- PostgreSQL 18:Routine Vacuuming
- PostgreSQL 18:VACUUM
- PostgreSQL 18:Vacuuming Configuration
- PostgreSQL 18:VACUUM Progress
- PostgreSQL 18:Visibility Map
- PostgreSQL 18:Heap-Only Tuples
- PostgreSQL 18:Table Partitioning
- PostgreSQL 18:amcheck
- Pigsty:PostgreSQL Monitoring
- Pigsty:PGSQL Dashboards
- Pigsty:
pig pgMaintenance Commands
实验文件
static/labs/ch28/
├── requirements.json
├── maintenance-contract.json
├── negative-cases.json
├── topology.mmd
├── lab-contract.md
├── capture.py
├── remote_experiment.py
├── exercise.py
├── validate.py
├── review.py
├── task.sh
└── maintenance-run.json执行:
static/labs/ch28/task.sh lint
export PG36_EVIDENCE_DIR=/private/path/to/new-empty-dir
static/labs/ch28/task.sh capture
static/labs/ch28/task.sh exercise
static/labs/ch28/task.sh verify
static/labs/ch28/task.sh reviewall 会按同一顺序执行。exercise 会产生真实 I/O、WAL、锁和短暂维护负载,只能在
已确认的一次性开发/测试环境运行。
本章验收
只有当你能交付以下证据,才算掌握本章:
trigger calculation
holder inventory
progress and blocker evidence
before/after physical + cumulative statistics
XID and MXID headroom
lock / space / WAL budget
maintenance command and rollback
index validity after rebuild
partition archive and restore manifest
exact cleanup or production change record“我跑了 VACUUM,没有报错”不是验收。
上一章:精益求精:参数调优与资源治理 · 返回下卷导读 · 下一章:移花接木:逻辑复制、迁移与异构同步 · 查看全书目录 · 查看索引中心