PostgreSQL 使用 MVCC 让读写尽量互不阻塞,但它也带来一个容易被忽略的代价:UPDATE 和 DELETE 不会立刻从物理文件中移除旧版本。旧元组只有在不再可能被任何事务看到后,才能由 VACUUM 清理并把空间标记为可复用。
Autovacuum 不是“可有可无的后台整理器”,而是 PostgreSQL 稳定运行的基础设施。它同时承担回收死元组、更新优化器统计信息、维护可见性映射以及防止事务 ID 回卷等工作。本文从触发公式、监控 SQL和调优方法三个角度,解释如何避免表膨胀与查询性能逐步恶化。
一、为什么会产生死元组
假设执行:
UPDATE orders SET status = 'paid' WHERE id = 1001;
PostgreSQL 通常不会在原位置覆盖旧行,而是创建新版本,并让旧版本在满足可见性条件前继续存在。这样,较早开始的事务仍可读取它应该看到的快照。
当旧版本不再对任何事务可见时,它就成为 dead tuple。若清理速度长期低于写入速度,会出现:
- 表和索引占用空间持续增加;
- 顺序扫描需要读取更多数据页;
- 缓存命中效率下降,磁盘 I/O 增加;
- 优化器统计信息过旧,可能选择错误执行计划;
- 维护操作耗时越来越长。
需要注意:普通 VACUUM 主要把空间交还给表内部复用,通常不会立即缩小操作系统看到的文件。VACUUM FULL 会重写整张表并归还空间,但需要额外磁盘空间和排他锁,因此不适合作为日常维护手段。
二、Autovacuum 的触发公式
对更新和删除产生的死元组,触发阈值可近似理解为:
vacuum 触发数 = autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor × 表估算行数
PostgreSQL 18 的默认值为:
autovacuum_vacuum_threshold = 50
autovacuum_vacuum_scale_factor = 0.2
一张估算为 1000 万行的表,默认需要约 200 万个更新或删除元组才触发一次 vacuum:
50 + 0.2 × 10,000,000 = 2,000,050
这对小表通常没问题,但对高频更新的大表可能过于宽松。分析统计信息的触发方式类似:
analyze 触发数 = autovacuum_analyze_threshold
+ autovacuum_analyze_scale_factor × 表估算行数
默认 autovacuum_analyze_threshold 为 50,autovacuum_analyze_scale_factor 为 0.1。
三、先监控,再调参数
1. 找出死元组较多的表
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
round(
100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0),
2
) AS dead_pct,
last_autovacuum,
last_autoanalyze,
autovacuum_count,
autoanalyze_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
不要只盯着 dead_pct:百万行大表的 5% 可能已经很多,而几十行小表的 50% 通常影响有限。应同时观察绝对数量、增长速度、表尺寸和业务延迟。
2. 查看表与索引大小
SELECT
n.nspname AS schema_name,
c.relname AS table_name,
pg_size_pretty(pg_table_size(c.oid)) AS table_size,
pg_size_pretty(pg_indexes_size(c.oid)) AS indexes_size,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 20;
pg_stat_user_tables 中的元组数是统计估算,不是严格实时计数。累计统计可能有短暂延迟,而且默认在同一事务中读取时会缓存到事务结束,因此排障时应避免在长事务里反复查询后误判数据没有变化。
3. 检查 Autovacuum 是否正在工作
SELECT
pid,
datname,
relid::regclass AS table_name,
phase,
heap_blks_total,
heap_blks_scanned,
heap_blks_vacuumed,
index_vacuum_count
FROM pg_stat_progress_vacuum;
同时检查全局配置:
SHOW autovacuum;
SHOW track_counts;
SHOW autovacuum_max_workers;
SHOW autovacuum_naptime;
SHOW autovacuum_vacuum_cost_delay;
SHOW autovacuum_vacuum_cost_limit;
四、针对热点大表做局部调优
相比一开始就修改全局配置,更稳妥的方式是为写入最频繁的表设置存储参数:
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_threshold = 1000,
autovacuum_analyze_scale_factor = 0.02,
autovacuum_analyze_threshold = 1000
);
若 orders 有 1000 万行,新的 vacuum 触发值约为:
1000 + 0.02 × 10,000,000 = 201,000
这比默认约 200 万行更及时,同时不会让所有表都采用同样激进的策略。调整后至少持续观察一个完整业务周期:
n_dead_tup是否形成可控波动而非持续上升;- autovacuum 的执行次数与单次持续时间;
- 数据盘 IOPS、吞吐和延迟;
- SQL 的 P95/P99 延迟;
- 表和索引尺寸是否趋于稳定。
五、为什么“调低阈值”仍可能无效
1. 长事务阻止清理
只要某个老快照仍可能看见旧版本,VACUUM 就不能安全移除它。可先排查长时间未结束的事务:
SELECT
pid,
usename,
state,
xact_start,
now() - xact_start AS xact_age,
wait_event_type,
wait_event,
left(query, 120) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;
应用连接池中的 idle in transaction、未提交的批处理、长期保持的报表事务,都是常见根因。此时继续降低 scale factor,只会让 vacuum 更频繁地尝试,却未必能回收空间。
2. 清理能力跟不上写入速度
Autovacuum 使用基于成本的延迟机制来降低后台维护对前台业务的 I/O 冲击。如果多张大表同时高频更新,worker 数量、成本限额或磁盘能力可能成为瓶颈。
调优顺序建议是:
- 先解决长事务和异常写入;
- 针对热点表降低触发阈值;
- 确认磁盘有余量后,再评估 worker 数量和成本参数;
- 每次只改一组参数,并保留前后监控数据。
不要只为“让 vacuum 尽快结束”就无限提高成本限额,否则维护 I/O 可能直接影响在线查询。
3. 误把内部可复用空间当成故障
普通 VACUUM 后文件不缩小,并不表示它没有工作。若表仍会持续写入,保留并复用这些空闲空间往往比频繁执行 VACUUM FULL 更合理。
只有在一次性大规模删除、确认未来不会很快重新增长,并且能够安排锁表维护窗口时,才考虑 VACUUM FULL、CLUSTER 或其他表重写方案。执行前必须预留接近表及索引重建所需的额外空间。
六、生产调优清单
- 保持 autovacuum 开启,不要把关闭它当成性能优化。
- 用
pg_stat_user_tables建立死元组数量、比例和清理时间的趋势图。 - 优先为热点大表设置表级参数,避免粗暴修改全局阈值。
- 同时监控磁盘 I/O 和业务延迟,防止维护任务挤压前台负载。
- 排查长事务、复制槽和其他可能维持旧快照的因素。
- 将普通 VACUUM 作为日常机制,把表重写当作有维护窗口的例外操作。
- 定期检查事务 ID 年龄,不能只关注磁盘膨胀。
总结
Autovacuum 的本质不是“定时清垃圾”,而是根据表的变更规模动态触发的一组维护任务。真正有效的优化,不是简单把参数调得更激进,而是让触发频率、清理吞吐和业务写入速度达到平衡。
面对表膨胀时,应先回答三个问题:死元组为什么持续产生、为什么未被及时清理、普通 VACUUM 回收的空间是否已经被业务复用。把这些问题与长事务、I/O 能力和表级阈值结合起来分析,通常比直接执行 VACUUM FULL 更安全,也更接近根因。