Featured image of post PostgreSQL Autovacuum:触发机制、表膨胀与参数调优
数据库

PostgreSQL Autovacuum:触发机制、表膨胀与参数调优

PostgreSQL Autovacuum:触发机制、表膨胀与参数调优

PostgreSQL 使用 MVCC 让读写尽量互不阻塞,但它也带来一个容易被忽略的代价:UPDATEDELETE 不会立刻从物理文件中移除旧版本。旧元组只有在不再可能被任何事务看到后,才能由 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 数量、成本限额或磁盘能力可能成为瓶颈。

调优顺序建议是:

  1. 先解决长事务和异常写入;
  2. 针对热点表降低触发阈值;
  3. 确认磁盘有余量后,再评估 worker 数量和成本参数;
  4. 每次只改一组参数,并保留前后监控数据。

不要只为“让 vacuum 尽快结束”就无限提高成本限额,否则维护 I/O 可能直接影响在线查询。

3. 误把内部可复用空间当成故障

普通 VACUUM 后文件不缩小,并不表示它没有工作。若表仍会持续写入,保留并复用这些空闲空间往往比频繁执行 VACUUM FULL 更合理。

只有在一次性大规模删除、确认未来不会很快重新增长,并且能够安排锁表维护窗口时,才考虑 VACUUM FULLCLUSTER 或其他表重写方案。执行前必须预留接近表及索引重建所需的额外空间。

六、生产调优清单

  1. 保持 autovacuum 开启,不要把关闭它当成性能优化。
  2. pg_stat_user_tables 建立死元组数量、比例和清理时间的趋势图。
  3. 优先为热点大表设置表级参数,避免粗暴修改全局阈值。
  4. 同时监控磁盘 I/O 和业务延迟,防止维护任务挤压前台负载。
  5. 排查长事务、复制槽和其他可能维持旧快照的因素。
  6. 将普通 VACUUM 作为日常机制,把表重写当作有维护窗口的例外操作。
  7. 定期检查事务 ID 年龄,不能只关注磁盘膨胀。

总结

Autovacuum 的本质不是“定时清垃圾”,而是根据表的变更规模动态触发的一组维护任务。真正有效的优化,不是简单把参数调得更激进,而是让触发频率、清理吞吐和业务写入速度达到平衡。

面对表膨胀时,应先回答三个问题:死元组为什么持续产生、为什么未被及时清理、普通 VACUUM 回收的空间是否已经被业务复用。把这些问题与长事务、I/O 能力和表级阈值结合起来分析,通常比直接执行 VACUUM FULL 更安全,也更接近根因。

参考资料

使用 Hugo 构建
主题 StackJimmy 设计