← 中文资源

简体中文版本

PostgreSQL — 表膨胀:autovacuum 跟不上

诊断死元组堆积、autovacuum 总也完不成的 PostgreSQL 表膨胀,按表调 autovacuum 参数,并用 pg_repack 重建。

阅读英文原文

症状

  统计视图自带诊断:n_dead_tup 987 万对 n_live_tup 1240 万——近一半表是尸体重量,而 last_autovacuum 停在膨胀发生之前。查询随天变慢(扫描连死元组一起读)、磁盘占用远超活跃数据量,是外显。责怪 autovacuum 参数之前先查谁钉住了地平线:pg_stat_activity 里按 xact_start 排序看 backend_xmin 非空的会话,一个从昨天就 idle in transaction 的会话让它开始之后的每个死元组对 vacuum 不可见——多少调参都救不了这个。

诊断思路

  先量化 n_dead_tup 与 n_live_tup 的比例加 last_autovacuum 的新鲜度;再查 xmin 地平线的钉住者(长事务、遗弃的复制槽、孤儿 prepared transaction);最后估算膨胀因子(磁盘大小对活跃数据量),量化重建的收益。autovacuum 被 DDL 锁冲突反复取消、每次从头再来,是第三种常见成因。

根因

  PostgreSQL 保留旧行版本直到所有并发读者都可能读完它们。当产生速度超过清理速度时死元组累积:清理地平线被长事务钉住、autovacuum 触发太晚或跑得太慢或被取消重启。表于是带着死重进入每次扫描。

修复与验证

  先修地平线:杀掉或用 idle_in_transaction_session_timeout 限制长事务。再针对热表调参:autovacuum_vacuum_scale_factor 降到 0.02(2% 死元组即触发而不是 20%)、cost limit 提到 2000 让它跑得更快。存量膨胀用 pg_repack 在线重建(不持排他锁);VACUUM FULL 只留给自己的维护窗口。为 HOT 更新设计:每更新一个被索引的列就强制新索引项、废掉 heap-only tuple 路径——把每写必更的计数列上的索引删掉。验证时重建加调参后,n_dead_tup 保持为 n_live_tup 的小比例、last_autovacuum 新鲜、表大小在整周生产流量下稳定。

代码示例

relname | n_live_tup | n_dead_tup | last_autovacuum
orders  | 12,402,118  | 9,871,335   | 2026-10-21 03:12:44
SELECT pid, state, xact_start, backend_xmin
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY xact_start;
ALTER TABLE events SET (
  autovacuum_vacuum_scale_factor = 0.02,   -- trigger at 2% dead, not 20%
  autovacuum_vacuum_cost_limit = 2000,     -- run faster when it does run
  autovacuum_vacuum_insert_scale_factor = 0.05
);
pg_repack -d shop -t events   -- rebuild without exclusive locks
-- if a counter column carries an index but updates every write:
DROP INDEX events_status_count_idx;