目录

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=50scale_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. 最佳实践清单

  1. 永远不要关闭 autovacuum
  2. 高频更新的大表,单独调低 autovacuum_vacuum_scale_factor(如 0.01~0.05)。
  3. 避免长事务和 idle in transaction——它们会钉住 xmin,阻止 VACUUM 回收死元组(见 MVCC多版本并发控制 第5节)。
  4. 大批量导入/删除后手动 VACUUM ANALYZE
  5. 监控 n_dead_tupage(datfrozenxid)
  6. 需要归还磁盘且膨胀严重时,优先用 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。