PostgreSQL 为什么要 vacuum?
更新时间:2026-09-18。这是问题驱动的短答页,完整机制与治理见深文 PostgreSQL VACUUM 与表膨胀治理 与 PostgreSQL 架构与 MVCC。
一句话回答
因为 PostgreSQL 的 MVCC 在更新/删除时不原地覆盖旧数据,而是插入新版本、把旧版本标记成「死元组」留在表里。这些死元组不会自动消失,必须靠 VACUUM 回收空间、更新统计信息,并在很老时冻结事务号,否则表和索引会持续膨胀、查询变慢,最终还可能撞上事务 id 回卷保护导致库变只读。
死元组从哪来
一次 UPDATE 在物理上等于「旧版本写 xmax 标记删除 + 插入一条新版本」,DELETE 则只把版本标记删除。这些被标记的旧版本对新事务不可见,但仍占着数据页和索引项:
| 不 vacuum 的后果 | 说明 |
|---|---|
| 表/索引膨胀 | 死元组占位,文件越来越大,顺序扫描与缓存命中变差 |
| 统计信息陈旧 | 优化器基于旧统计选错计划(该走索引却全表扫) |
| 索引堆积 | 死索引项让索引变胖、回表变多 |
| 事务号回卷风险 | 很老的元组不冻结,age 逼近上限会触发强制只读保护 |
VACUUM 具体做什么
- 回收死元组占用的位置,标记为可复用(注意:普通 VACUUM 不会缩小物理文件,真正还空间给操作系统要用
VACUUM FULL或在线的 pg_repack)。 - 清理死索引项、更新可见性图(visibility map),帮助实现 Index Only Scan。
- 顺手
ANALYZE更新优化器统计信息。 - 把足够老的元组事务号标记为 frozen,防止 XID 回卷。
生产中一般不用手工调,autovacuum 会按「死元组数超过阈值 + 比例 × 行数」自动触发。
为什么有时 vacuum 了还是膨胀
最常见根因是长事务:只要有一个很老的活跃快照(比如忘了提交的事务、idle in transaction、落后的复制槽/hot_standby_feedback),那些旧版本「可能还被它看到」,vacuum 就不能回收。排查命令:
-- 谁拖住了最老事务
SELECT pid, state, now()-xact_start AS age, backend_xmin, left(query,80)
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY age(backend_xmin) DESC;
-- autovacuum 进度
SELECT relid::regclass, phase, heap_blks_scanned, heap_blks_total
FROM pg_stat_progress_vacuum;另一个常见原因是默认 autovacuum_vacuum_scale_factor=0.2 对大表太粗(要积 20% 死元组才清),大表应单独调小到 0.02~0.05。
实操建议
- 别关 autovacuum;大表调小 scale factor、必要时提高 cost limit。
- 监控最长事务年龄与
age(relfrozenxid),别让复制槽/长事务停摆清理。 - 热点更新表设
fillfactor(如 85)提升 HOT,减少新版本进索引。 - 已经明显膨胀再用 pg_repack/REINDEX CONCURRENTLY 在线回收,普通 VACUUM 不缩文件。
相关与延伸
想看数据结构层面的 xmin/xmax、快照可见性与 HOT,读 PostgreSQL 架构与 MVCC;想看膨胀量化、pgstattuple、pg_repack、参数清单,读 PostgreSQL VACUUM 与表膨胀治理;MySQL 对照(undo purge)见 MySQL 锁与事务。
总结
PostgreSQL 要 vacuum,是因为 MVCC 更新「插新留旧」产生死元组,不回收就会表和索引膨胀、统计陈旧、并最终面临事务号回卷;vacuum 负责把死元组位置标为可复用、更新可见性图与统计、冻结老 XID,生产靠 autovacuum 自动执行,但长事务、失效复制槽和过大的 scale factor 会让它清不动,需要专门监控与调参。