pgsql-27 VACUUM与表膨胀
1. 概述
VACUUM 是 PostgreSQL 独有且必须理解的运维主题,也是 PG 面试与 MySQL 转 PG 的开发者最容易踩坑的地方。它源于 PG 的 MVCC 实现方式(见 MVCC多版本并发控制):
PostgreSQL 的 UPDATE/DELETE 不会立即删除旧数据,而是把旧版本标记为"死元组(dead tuple)"。这些死元组占着空间不释放,就是表膨胀(table bloat)。VACUUM 的职责就是回收死元组占用的空间。
MySQL(InnoDB) 因为旧版本存在 undo log、由 purge 线程自动清理,DBA 通常感知不到这个过程;而 PG 把死元组留在表里,必须靠 VACUUM 处理,所以这是 PG 特有的关注点。
2. 死元组与表膨胀的成因
-- 一次 UPDATE 的真实过程
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 实际:旧版本元组 xmax=当前事务号(变成死元组)
-- 插入新版本元组 xmin=当前事务号
- 高频 UPDATE / DELETE 的表会持续产生死元组。
- 死元组不仅占数据页空间,还会:
- 让表文件变大(磁盘浪费);
- 降低查询效率(扫描时要跳过大量死元组);
- 让索引也膨胀。
-- 查看表的死元组数量和膨胀情况
SELECT relname,
n_live_tup AS 活元组,
n_dead_tup AS 死元组,
round(n_dead_tup::numeric / nullif(n_live_tup, 0), 4) AS 死活比,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
3. VACUUM 的三种形态
3.1 VACUUM(普通)
标记死元组占用的空间为"可重用",供该表后续 INSERT/UPDATE 复用。不会把空间还给操作系统,但表不再继续膨胀。
VACUUM accounts;
VACUUM (VERBOSE) accounts; -- 打印详情
- 不锁表(只持 SHARE UPDATE EXCLUSIVE,不阻塞读写),可在线运行。
- 这是最常用、日常应该频繁做的。
3.2 VACUUM FULL(重写整表)
重写整张表到新文件,彻底消除膨胀并把空间归还操作系统。
VACUUM FULL accounts;
- ⚠️ 持有 ACCESS EXCLUSIVE 锁,全程锁表(读写都阻塞),且需要额外磁盘空间存新表。
- 只在膨胀极严重、且能接受停机窗口时使用。
- 在线替代方案:用
pg_repack扩展(见 扩展插件总览),不长时间锁表地重建表。
3.3 ANALYZE(更新统计信息)
收集表的数据分布统计信息,供查询优化器估算行数、选择执行计划。它不清理死元组,但常与 VACUUM 一起做。
ANALYZE accounts;
VACUUM ANALYZE accounts; -- 清理 + 更新统计,一步到位
统计信息过期会导致优化器估算错误、选错执行计划(比如该走索引却全表扫描)。大批量导入数据后应手动 ANALYZE。
3.4 三者对比
| 操作 | 清理死元组 | 归还磁盘 | 锁表 | 更新统计 |
|---|---|---|---|---|
VACUUM |
✅(标记可重用) | ❌ | 否 | ❌ |
VACUUM FULL |
✅(彻底) | ✅ | 是(阻塞一切) | ✅ |
ANALYZE |
❌ | ❌ | 否 | ✅ |
4. autovacuum 自动清理
PostgreSQL 有后台 autovacuum 守护进程,自动对满足条件的表执行 VACUUM 和 ANALYZE,无需人工干预(默认开启,生产环境绝不要关闭)。
4.1 触发阈值
当"死元组数"超过阈值时触发:
触发阈值 = autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor × 表行数
默认 threshold=50、scale_factor=0.2(即死元组超过约 20% 时触发)。
4.2 大表的问题与调优
默认 scale_factor=0.2 对大表太迟钝:一个 1 亿行的表要累积 2000 万死元组才触发一次,期间已严重膨胀。生产常做两件事:
-- 1) 对大表单独调低 scale_factor,让它更早触发
ALTER TABLE big_table SET (autovacuum_vacuum_scale_factor = 0.02);
-- 2) 调大 autovacuum 的资源,让它清理更快
-- postgresql.conf:
-- autovacuum_max_workers = 5
-- autovacuum_vacuum_cost_limit = 2000 # 默认较保守,可调大加速
-- 查看 autovacuum 是否在运行
SELECT pid, query, now()-xact_start AS dur
FROM pg_stat_activity WHERE query LIKE 'autovacuum%';
5. 事务 ID 回卷(Transaction ID Wraparound)
这是 PG 最严重、也是面试进阶考点。事务 ID(XID)是 32 位,约 42 亿个后会回卷。可见性判断依赖"XID 的新旧比较",若不处理,回卷会导致过去的数据突然变成"来自未来"而不可见——数据看起来消失了。
防护机制:freeze(冻结)。VACUUM 会把足够老的元组的 xmin 标记为"冻结(frozen)",表示"对所有事务永远可见",从而不再参与 XID 比较。这个过程由 VACUUM 承担,因此 VACUUM 不仅是清理空间,还是防止 XID 回卷的关键。
-- 监控距离回卷还有多少 XID(age 越大越危险)
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database ORDER BY xid_age DESC;
- 接近
autovacuum_freeze_max_age(默认 2 亿)时会强制触发 anti-wraparound autovacuum,即使你关了 autovacuum 也会跑。 - 若长期忽视(如长事务持续阻止 freeze),PG 会进入只读保护模式强制要求 VACUUM,属于严重线上事故。
6. 最佳实践清单
- 永远不要关闭 autovacuum。
- 高频更新的大表,单独调低
autovacuum_vacuum_scale_factor(如 0.01~0.05)。 - 避免长事务和 idle in transaction——它们会钉住 xmin,阻止 VACUUM 回收死元组(见 MVCC多版本并发控制 第5节)。
- 大批量导入/删除后手动
VACUUM ANALYZE。 - 监控
n_dead_tup和age(datfrozenxid)。 - 需要归还磁盘且膨胀严重时,优先用
pg_repack而非VACUUM FULL。
7. 面试高频问答
Q1:什么是表膨胀?为什么 PostgreSQL 会有而 MySQL 没有? PG 的 MVCC 把旧版本作为死元组留在堆表里,UPDATE/DELETE 不断产生死元组占用空间形成膨胀,需要 VACUUM 回收;MySQL 旧版本放在 undo log 由 purge 线程清理,DBA 通常感知不到,因此没有等价的"表膨胀"运维负担。
Q2:VACUUM 和 VACUUM FULL 区别? VACUUM 标记死元组空间可重用、不锁表、不归还磁盘;VACUUM FULL 重写整表、彻底回收并归还磁盘,但全程 ACCESS EXCLUSIVE 锁表。日常用前者,前者救不了的重度膨胀才考虑后者或 pg_repack。
Q3:autovacuum 是什么?能关吗? 后台自动执行 VACUUM/ANALYZE 的守护进程,按死元组比例阈值触发。生产绝不能关,否则表膨胀失控且面临 XID 回卷风险。
Q4:什么是事务 ID 回卷?怎么防? XID 是 32 位会回卷,回卷后旧数据可能变得不可见导致"数据消失"。VACUUM 通过 freeze 把老元组标记为永久可见来防护,接近阈值会强制触发 anti-wraparound autovacuum。
Q5:长事务为什么会加剧表膨胀? 长事务的旧快照钉住了一个较老的 xmin,VACUUM 不能回收"可能还被它看到"的死元组,导致死元组持续堆积、表不断膨胀。
Q6:ANALYZE 有什么用? 更新表的统计信息(数据分布),供查询优化器估算行数、选择执行计划;统计过期会导致选错计划。大批量数据变更后应及时 ANALYZE。
8. 下一步
学习 配置参数调优,从内存与并发参数层面优化 PG。
xingliuhua