Skip to content

AutoVacuum 配置 ​

生产环境里,没有人会记得每天手工跑 VACUUM。PostgreSQL 用一个后台守护进程把这个动作自动化,它就是 autovacuum(自动清理)。它会按「死元组累积到多少」的阈值自动触发清理与统计收集,并在事务 ID 濒临回卷时强制介入。

问题是:autovacuum 的默认参数是为小库设计的。在一张亿级行的大表上,默认阈值意味着要积累两千万个死元组才会清理一次——那时表已经膨胀好几倍,单次清理也变成一场灾难。所以「autovacuum 跟不上」几乎总是配置问题,而不是它不够努力。

本节从参数配置与调优的角度讲清 autovacuum:进程架构、触发条件怎么算、每个参数该设多少、按表覆盖怎么做、怎么监控、为什么经常「跟不上」、以及如何排障。进程本身的调度机制可参考架构章节。

前置知识

阅读本节前,建议先了解:VACUUM FULL 与重建

基础概念 ​

进程架构:launcher 与 worker ​

autovacuum 不是一个进程,而是一个调度器 + 若干工作进程:

text
postmaster
   │
   ├── autovacuum launcher(1 个,常驻)
   │      │
   │      ├─ 每隔 autovacuum_naptime 醒来一次
   │      ├─ 从共享内存统计里读取各表的死元组/修改计数
   │      ├─ 判断哪些库、哪些表「需要清理」
   │      └─ 启动 worker 并分配任务
   │
   ├── autovacuum worker #1  ← 实际执行 VACUUM / ANALYZE
   ├── autovacuum worker #2
   └── autovacuum worker #3   (数量上限 = autovacuum_max_workers)
组件作用相关参数
Launcher定时唤醒、评估、派发任务autovacuum_naptime
Worker真正执行 VACUUM/ANALYZEautovacuum_max_workers
工作进程槽位预留的后端槽位,允许运行时上调 worker 数autovacuum_worker_slots(18+)
统计收集worker 判断依据的数据来源track_counts(必须为 on)

每个 worker 最多同时处理一张表。3 个 worker 意味着最多 3 张表在并行清理——这是理解「为什么大库排不过来」的关键。

sql
-- 看看当前有几个 autovacuum 进程在跑
SELECT pid, backend_type, state, query
FROM pg_stat_activity
WHERE backend_type = 'autovacuum worker';

触发条件:一张表什么时候会被清理 ​

autovacuum 用阈值 + 比例的组合公式判断,而不是「固定死元组数」:

text
触发自动 VACUUM(死元组维度):
    死元组数 > autovacuum_vacuum_threshold
             + autovacuum_vacuum_scale_factor × 表估算行数

触发自动 VACUUM(插入维度,PG 13+):
    新插入元组数 > autovacuum_vacuum_insert_threshold
                 + autovacuum_vacuum_insert_scale_factor × 表估算行数

触发自动 ANALYZE:
    被修改元组数 > autovacuum_analyze_threshold
                 + autovacuum_analyze_scale_factor × 表估算行数

默认值:

参数默认值含义
autovacuum_vacuum_threshold50固定基数(死元组)
autovacuum_vacuum_scale_factor0.2表大小的百分比
autovacuum_vacuum_insert_threshold1000(13+)纯 INSERT 触发的固定基数
autovacuum_vacuum_insert_scale_factor0.2(13+)纯 INSERT 触发的百分比
autovacuum_analyze_threshold50固定基数(被修改元组)
autovacuum_analyze_scale_factor0.1表大小的百分比

代入不同规模的表,看阈值变化:

text
表行数           VACUUM 触发所需死元组     ANALYZE 触发所需修改
─────────────    ──────────────────────    ──────────────────────
1,000            50 + 0.2×1,000    = 250    50 + 0.1×1,000   = 150
100,000          50 + 0.2×100,000  = 20,050 50 + 0.1×100,000 = 10,050
10,000,000       50 + 0.2×1e7      = 2e6    50 + 0.1×1e7     = 1e6
100,000,000      50 + 0.2×1e8      = 2e7    50 + 0.1×1e8     = 1e7

百分比阈值在大表上会「失效」

1 亿行的表要积累 2000 万个死元组才触发一次 autovacuum。到那时:

  • 表体积可能已是实际数据的数倍(膨胀)。
  • 单次清理要处理 2000 万个死元组,耗时极长、I/O 冲击极大。
  • 清理期间又产生新的死元组,永远追不上 → 「越不清理越难清理」。

因此大表必须用表级参数覆盖全局阈值,把触发规模压到「几万到几十万行」这个量级。

为什么 autovacuum 常常「跟不上」 ​

原因表现对策
阈值比例太大大表长期不清理,n_dead_tup 持续增长按表降低 scale_factor
worker 数量太少表多时排队,last_autovacuum 很旧提高 autovacuum_max_workers
成本延迟太保守单次清理跑很久,占用 worker 时间降低 autovacuum_vacuum_cost_delay、提高 cost_limit
长事务压制oldest xmin 不推进,清理无法回收治理长事务/idle in transaction
复制槽未推进类似长事务,钉住 oldest xmin检查 pg_replication_slots
表被关闭了 autovacuum单表 autovacuum_enabled = false谨慎使用该开关
被表锁挡住worker 等锁超时后放弃并记日志加 SKIP_LOCKED 思路或排查 DDL 冲突
只插入的表不触发清理纯 INSERT 场景在 PG 13 前不触发PG 13+ 的 insert 阈值可解决

VACUUM 与 ANALYZE 是两件独立的事 ​

autovacuum 同时负责这两项,但它们的触发条件与目的完全不同:

维度自动 VACUUM自动 ANALYZE
目的回收死元组、冻结、更新 VM更新优化器统计信息
触发依据死元组数(+ PG13 的插入数)被修改元组数
默认 scale factor0.20.1
只看插入仅 PG 13+ 支持支持(修改即计数)
不做会怎样表膨胀、回卷风险执行计划选错、变慢

一个常见误区是「表不膨胀就不用管 autovacuum」。实际上统计信息过期导致的全表扫描,往往比膨胀更早影响业务。所以两套阈值都要调。

语法与用法 ​

全局参数一览 ​

在 postgresql.conf 或通过 ALTER SYSTEM 设置:

参数默认值作用生效方式
autovacuumon总开关(关闭后仍会为防回卷启动 worker)重启
autovacuum_max_workers3worker 数量上限重启(18+ 可在 autovacuum_worker_slots 范围内动态调整)
autovacuum_worker_slots16(18+)预留的后端槽位数,允许运行时上调 worker 数重启
autovacuum_naptime1minlauncher 唤醒间隔重载
autovacuum_vacuum_threshold50死元组固定阈值重载
autovacuum_vacuum_scale_factor0.2死元组比例阈值重载
autovacuum_vacuum_insert_threshold1000(13+)插入固定阈值重载
autovacuum_vacuum_insert_scale_factor0.2(13+)插入比例阈值重载
autovacuum_vacuum_max_threshold100000000(18+)死元组绝对上限,超过必定触发重载
autovacuum_analyze_threshold50修改元组固定阈值重载
autovacuum_analyze_scale_factor0.1修改元组比例阈值重载
autovacuum_vacuum_cost_delay2ms(12+)worker 节流休眠时长;-1 表示沿用 vacuum_cost_delay重载
autovacuum_vacuum_cost_limit-1worker 成本上限;-1 表示沿用 vacuum_cost_limit(默认 200)重载
autovacuum_freeze_max_age200000000强制防回卷清理的年龄上限重启
autovacuum_multixact_freeze_max_age400000000多事务 ID 的对应上限重启
autovacuum_vacuum_scale_factor—见上—
log_autovacuum_min_duration-1(不记录)超过该毫秒数的 autovacuum 记入日志;0 表示全记重载
track_countson统计计数开关,必须为 on重启

PostgreSQL 18 的两个新参数

autovacuum_vacuum_max_threshold:给死元组数设一个绝对上限。超过它时无论如何都触发清理,弥补了「百分比阈值在大表上永远不触发」的缺陷。默认 1 亿。

autovacuum_worker_slots:预留 autovacuum 工作进程可用的后端槽位(默认 16)。设置之后,autovacuum_max_workers 可以在运行时上调到该值以内,不必重启。

sql
-- 先留出槽位(需重启),之后即可动态调整 worker 数
ALTER SYSTEM SET autovacuum_worker_slots = 16;
-- 重启后:
ALTER SYSTEM SET autovacuum_max_workers = 8;
SELECT pg_reload_conf();
SHOW autovacuum_max_workers;

按表覆盖(最重要的一节) ​

绝大多数字段都可以在表级覆盖,这是解决「某张大表不清理」的首选手段,因为它比调全局参数精准得多:

sql
ALTER TABLE 表名 SET (
    参数 = 值
);

也可以在建表时写入:

sql
CREATE TABLE orders (
    ...
) WITH (
    autovacuum_vacuum_scale_factor = 0.01,
    autovacuum_analyze_scale_factor = 0.005
);

可用的表级参数:

表级参数等价全局参数说明
autovacuum_enabledautovacuum是否对该表启用自动清理
autovacuum_vacuum_threshold同名死元组固定阈值
autovacuum_vacuum_scale_factor同名死元组比例阈值
autovacuum_vacuum_insert_threshold同名插入固定阈值(13+)
autovacuum_vacuum_insert_scale_factor同名插入比例阈值(13+)
autovacuum_analyze_threshold同名修改固定阈值
autovacuum_analyze_scale_factor同名修改比例阈值
autovacuum_vacuum_cost_delay同名该表的节流延迟
autovacuum_vacuum_cost_limit同名该表的成本上限
autovacuum_freeze_min_agevacuum_freeze_min_age冻结最小年龄
autovacuum_freeze_max_age同名防回卷年龄上限
autovacuum_freeze_table_agevacuum_freeze_table_age触发全表冻结的年龄
autovacuum_multixact_freeze_max_age同名多事务 ID 上限
log_autovacuum_min_duration同名该表的日志阈值
vacuum_truncatevacuum_truncate(18+)是否允许截断尾部空页
vacuum_index_cleanupINDEX_CLEANUP是否清理索引死项

查看某张表实际生效的参数:

sql
SELECT relname, reloptions
FROM pg_class
WHERE relname = 'orders';
text
 relname |                                    reloptions
---------+----------------------------------------------------------------------------------
 orders  | {autovacuum_vacuum_scale_factor=0.01,autovacuum_analyze_scale_factor=0.005}

按表调优的典型配方 ​

不同负载的表,参数取向完全不同:

sql
-- ① 高频更新的大表(订单、计数器、状态表)
--    关键:让阈值落在几万行量级,且清理要跑得快
ALTER TABLE orders SET (
    autovacuum_vacuum_scale_factor  = 0.01,    -- 1%,而非默认 20%
    autovacuum_analyze_scale_factor = 0.005,   -- 0.5%
    autovacuum_vacuum_cost_delay    = 2,       -- SSD 上可以更激进
    autovacuum_vacuum_cost_limit    = 1000
);

-- ② 只插入、几乎不更新的日志/事件表(PG 13+)
--    靠 insert 阈值触发清理,同时冻结以免年龄老化
ALTER TABLE event_log SET (
    autovacuum_vacuum_insert_scale_factor = 0.05,
    autovacuum_freeze_max_age             = 500000000
);

-- ③ 极小但更新极频繁的配置/计数表
--    固定阈值就够了,比例阈值对大表才有意义
ALTER TABLE counters SET (
    autovacuum_vacuum_threshold  = 100,
    autovacuum_analyze_threshold = 100
);

-- ④ 只在维护窗口清理的表(谨慎!)
ALTER TABLE archive_2024 SET (autovacuum_enabled = off);

关闭 autovacuum 几乎总是错的

autovacuum_enabled = false 只关掉了「常规清理」,防回卷的强制清理仍会启动。但同时你会失去空间回收与统计更新,导致表膨胀、计划失真,最后只能用 VACUUM FULL 收拾。

真正想「避免 autovacuum 在高峰期打扰业务」时,正确做法是调低 autovacuum_vacuum_cost_limit 让它更温柔,而不是关掉它。

临时全局压制的写法 ​

有时需要在批量导入期间临时放宽节奏(而不是关闭):

sql
-- 批量导入窗口:暂停 autovacuum 的调度(仅影响常规清理)
ALTER SYSTEM SET autovacuum = off;
SELECT pg_reload_conf();
-- 导入完成后立即恢复并主动清理
ALTER SYSTEM SET autovacuum = on;
SELECT pg_reload_conf();
VACUUM (ANALYZE) big_import_table;
sql
-- 更温和的替代:只让它变慢,不关闭
ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 50;    -- 从 200 降到 50
SELECT pg_reload_conf();
-- 结束后恢复
ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 200;
SELECT pg_reload_conf();

详细说明 ​

成本延迟机制:为什么 autovacuum「看起来很慢」 ​

autovacuum 对 I/O 消耗做了主动节流,避免把业务 I/O 挤掉。机制是一个「成本计数器」:

text
每个 worker 有自己的成本余额(上限 = autovacuum_vacuum_cost_limit,默认沿用 vacuum_cost_limit = 200)

发现一个死元组            扣 vacuum_cost_page_hit  (默认 1,缓冲区命中)
从磁盘读一个页            扣 vacuum_cost_page_miss (默认 2)
弄脏一个页(需要写回)     扣 vacuum_cost_page_dirty(默认 20)

余额耗尽 → 休眠 autovacuum_vacuum_cost_delay(默认 2ms)→ 余额重置

例如每处理一页平均消耗 30 点成本、cost_limit = 200:

text
每处理约 6~7 页就要休眠 2ms
→ 理论吞吐上限约 3000~3500 页/秒

如果盘慢、或者 worker 少,实际远低于此

调优方向:

目标调整
让清理跑得更快、更快追平提高 autovacuum_vacuum_cost_limit(如 1000),或降低 autovacuum_vacuum_cost_delay
让清理对业务影响更小降低 cost_limit(如 50)或提高 cost_delay
只让个别大表更激进用表级 autovacuum_vacuum_cost_limit 覆盖

SSD / NVMe 上的推荐取向

机械盘时代默认的「温柔」设置,在 NVMe 上显得过于保守。常见做法是把延迟降到 0~2ms、成本上限提到 1000~2000,让 autovacuum 更快完成单表清理,从而释放 worker。注意 cost_limit 是所有 worker 共享的额度(总额度按 worker 数分摊)。

长事务为什么能「压制」autovacuum ​

VACUUM 只能回收「对所有活跃快照都不可见」的元组。判定依据是 oldest xmin——所有活跃事务中最老的快照边界。

text
时间轴 →

事务 A 开始(快照固定在 T1)━━━━━━━━━━━━━━━━━━━━━━━━━ A 提交
                    │
        在 T1 之后被 UPDATE 掉的元组
        对 A 来说「仍需要看到」→ 不能回收
                    │
     VACUUM 跑了 N 次,每次都报告:
     "N dead row versions cannot be removed yet, oldest xmin: ..."

所以排查 autovacuum 无效时,第一件事是查长事务:

sql
-- 按事务开始时间排序,找出最老的事务
SELECT pid, usename, state,
       now() - xact_start AS xact_age,
       now() - state_change AS state_age,
       wait_event_type, wait_event,
       left(query, 60) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start
LIMIT 10;
text
  pid  | usename |        state         | xact_age  |     wait_event     |        query
-------+---------+----------------------+-----------+--------------------+---------------------
 41900 | app     | idle in transaction  | 02:47:18  | ClientRead         | UPDATE orders SET ...
 41882 | app     | active               | 00:00:03  | DataFileRead       | SELECT ...

上例中 41900 是一个「开了事务却没有提交」的会话,已经挂了 2 小时 47 分——它独自让所有清理都失去意义。这类连接通常源于应用没有正确管理事务(例如忘了 commit,或者连接池把连接借出后长期不归还)。

除了长事务,未推进的复制槽与遗留的预备事务也会钉住 oldest xmin:

sql
-- 复制槽:restart_lsn 长时间不动会一直保留旧数据
SELECT slot_name, slot_type, active, restart_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots
ORDER BY restart_lsn;

-- 遗留的预备事务
SELECT gid, owner, database, prepared, now() - prepared AS age
FROM pg_prepared_xacts
ORDER BY prepared;

如何观察 autovacuum 是否在正常工作 ​

核心数据来自 pg_stat_user_tables:

sql
SELECT relname,
       n_live_tup,
       n_dead_tup,
       n_mod_since_analyze,
       last_vacuum,
       last_autovacuum,
       vacuum_count,
       autovacuum_count,
       last_analyze,
       last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;
字段含义判读要点
n_live_tup估算活元组数与 count(*) 的偏差反映统计新鲜度
n_dead_tup估算死元组数长期高企 → 清理跟不上
n_mod_since_analyze上次 ANALYZE 后的修改数很大 → 统计过期
last_vacuum上次手工 VACUUM 时间手工动作记录
last_autovacuum上次自动清理时间很久以前 → 一直没触发
vacuum_count手工清理次数累计值
autovacuum_count自动清理次数累计值
last_autoanalyze上次自动分析时间统计新鲜度

这些字段是「上次时间」而不是「是否正在跑」

last_autovacuum 反映的是上一次完成的时间。想知道当前是否正在清理,要查 pg_stat_activity 与 pg_stat_progress_vacuum。

PostgreSQL 18 还新增了耗时统计,便于评估 autovacuum 的资源占比:

sql
-- PostgreSQL 18+:累计清理/分析耗时(毫秒)
SELECT relname,
       total_vacuum_time, total_autovacuum_time,
       total_analyze_time, total_autoanalyze_time
FROM pg_stat_user_tables
ORDER BY total_autovacuum_time DESC
LIMIT 5;
text
 relname | total_vacuum_time | total_autovacuum_time | total_analyze_time | total_autoanalyze_time
---------+-------------------+-----------------------+--------------------+------------------------
 orders  |                 0 |               1284320 |                  0 |                  38410
 products|                 0 |                 42180 |                  0 |                   9120

观察正在进行的清理 ​

sql
SELECT p.pid,
       a.query,
       p.relid::regclass AS table_name,
       p.phase,
       p.heap_blks_total,
       p.heap_blks_scanned,
       round(p.heap_blks_scanned * 100.0 / greatest(p.heap_blks_total, 1), 1) AS scan_pct,
       p.index_vacuum_count,
       now() - a.xact_start AS running_for
FROM pg_stat_progress_vacuum p
JOIN pg_stat_activity a USING (pid);
text
  pid  | table_name |      phase       | heap_blks_total | heap_blks_scanned | scan_pct | index_vacuum_count | running_for
-------+------------+------------------+-----------------+-------------------+----------+--------------------+-------------
 41237 | orders     | vacuuming heap   |           12800 |             12800 |    100.0 |                  1 | 00:00:41.213

结合 backend_type 可以判断是自动还是手工:

sql
SELECT pid, backend_type, query
FROM pg_stat_activity
WHERE backend_type LIKE 'autovacuum%';

回卷防护:autovacuum 的「最后防线」 ​

即使 autovacuum = off,当某张表的 age(relfrozenxid) 超过 autovacuum_freeze_max_age(默认 2 亿)时,PostgreSQL 仍会强制启动 worker 做防回卷清理。日志里会出现:

text
LOG:  automatic vacuum of table "demo.public.orders": index scans: 1
      ...
      to prevent wraparound
sql
-- 查看年龄最大的表,判断是否需要人工介入
SELECT c.oid::regclass AS table_name,
       age(c.relfrozenxid) AS xid_age,
       current_setting('autovacuum_freeze_max_age')::bigint AS freeze_max_age,
       round(age(c.relfrozenxid) * 100.0
             / current_setting('autovacuum_freeze_max_age')::numeric, 1) AS pct_of_limit
FROM pg_class c
WHERE c.relkind = 'r'
  AND c.relnamespace NOT IN ('pg_catalog'::regnamespace, 'information_schema'::regnamespace)
ORDER BY age(c.relfrozenxid) DESC
LIMIT 10;
text
 table_name | xid_age   | freeze_max_age | pct_of_limit
------------+-----------+----------------+--------------
 orders     | 152340982 |      200000000 |         76.2
 event_log  |  88421003 |      200000000 |         44.2

防回卷清理「优先级最高、锁风险也最高」

当年龄逼近上限时,autovacuum 会忽略成本延迟限制全速清理,并可能触发全表冻结扫描(aggressive vacuum)。这会带来明显 I/O 冲击,甚至阻塞业务。因此不能把回卷防护当作日常清理手段——它只是兜底。

如果发现表已经逼近上限,应该主动处理:

sql
-- 在低峰期手工冻结
VACUUM (FREEZE, ANALYZE, VERBOSE) orders;

细节见 冻结事务 ID(FREEZE)。

监控与告警建议 ​

一套最小可用的巡检 SQL:

sql
-- ① 死元组比例过高的表(可能清理跟不上)
SELECT relname,
       n_live_tup, n_dead_tup,
       round(n_dead_tup * 100.0 / greatest(n_live_tup + n_dead_tup, 1), 2) AS dead_pct,
       last_autovacuum
FROM pg_stat_user_tables
WHERE n_live_tup + n_dead_tup > 10000
  AND n_dead_tup * 100.0 / greatest(n_live_tup + n_dead_tup, 1) > 10
ORDER BY dead_pct DESC;
sql
-- ② 统计信息过期的表(影响执行计划)
SELECT relname,
       n_mod_since_analyze,
       n_live_tup,
       round(n_mod_since_analyze * 100.0 / greatest(n_live_tup, 1), 2) AS mod_pct,
       last_autoanalyze
FROM pg_stat_user_tables
WHERE n_mod_since_analyze * 100.0 / greatest(n_live_tup, 1) > 20
ORDER BY mod_pct DESC;
sql
-- ③ 事务 ID 年龄逼近危险线(最高优先级告警)
SELECT c.oid::regclass AS table_name,
       age(c.relfrozenxid) AS xid_age,
       round(age(c.relfrozenxid) * 100.0
             / current_setting('autovacuum_freeze_max_age')::numeric, 1) AS pct_of_limit
FROM pg_class c
WHERE c.relkind = 'r'
  AND c.relnamespace NOT IN ('pg_catalog'::regnamespace, 'information_schema'::regnamespace)
  AND age(c.relfrozenxid) > current_setting('autovacuum_freeze_max_age')::bigint * 0.75
ORDER BY age(c.relfrozenxid) DESC;
sql
-- ④ 库级年龄(回卷风险的总览)
SELECT datname,
       age(datfrozenxid) AS db_xid_age,
       round(age(datfrozenxid) * 100.0 / 2000000000.0, 2) AS pct_to_wraparound
FROM pg_database
ORDER BY age(datfrozenxid) DESC;

推荐的告警阈值:

指标警告严重动作
单表死元组比例> 10% 持续 1 小时> 30%检查 autovacuum 参数、长事务
n_mod_since_analyze / n_live_tup> 20%> 50%手工 ANALYZE 或调小 analyze 阈值
age(relfrozenxid)> 1.5 亿(75%)> 1.8 亿(90%)手工 VACUUM FREEZE
age(datfrozenxid)> 15 亿> 18 亿立即介入,防止停机
最老事务时长> 5 分钟> 30 分钟排查应用事务管理

把 autovacuum 行为写入日志 ​

默认 autovacuum 静默运行,出问题无从追溯。建议至少记录「超过 1 秒的清理」:

sql
ALTER SYSTEM SET log_autovacuum_min_duration = 1000;   -- 单位毫秒
SELECT pg_reload_conf();

日志形态:

text
LOG:  automatic vacuum of table "demo.public.orders": index scans: 1
      pages: 0 removed, 322 remain, 0 skipped due to pins, 0 skipped frozen
      tuples: 4210 removed, 11880 remain, 0 are dead but not yet removable
      removable cutoff: 7341, which was 0.03s ago
      buffer usage: 812 hits, 96 misses, 21 dirtied
      avg read rate: 12.4 MB/s, avg write rate: 3.1 MB/s
      system usage: CPU: user: 0.04 s, system: 0.01 s, elapsed: 0.31 s

关键字段判读:

字段含义异常信号
tuples: N removed本次回收的死元组数长期为 0 但 n_dead_tup 高 → 被长事务压制
0 are dead but not yet removable因快照无法回收的数量大于 0 说明有老快照
removable cutoff ... which was Xs ago回收截断点对应的时间值很旧 → oldest xmin 被钉住
pages: N skipped due to pins因被并发访问而跳过的页数值大说明并发压力影响清理效率
avg read/write rate实际 I/O 速率远低于磁盘能力 → 节流过于保守
elapsed本次耗时持续很长 → 需要提高 cost_limit

PostgreSQL 18 起,日志还会额外输出 VACUUM 的延迟(delay)时间与 WAL 缓冲区用量,便于区分「真在干活」与「大部分时间在休眠」。

实战示例 ​

场景一:诊断「autovacuum 好像没在工作」 ​

sql
-- 第 1 步:从表级统计看到底有没有跑过
SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC LIMIT 5;
text
 relname | n_live_tup | n_dead_tup |       last_autovacuum       |      last_autoanalyze
---------+------------+------------+-----------------------------+-----------------------------
 orders  |    9820411 |    4832110 | 2026-09-02 03:14:02.114+08  | 2026-09-01 22:41:19.882+08
 products|       2000 |         38 | 2026-09-28 09:58:02.114+08  | 2026-09-28 09:58:02.114+08

orders 有 980 万活元组、483 万死元组,而上次自动清理在 27 天前——典型的大表阈值过高问题。

sql
-- 第 2 步:确认阈值——980 万行 × 0.2 = 约 196 万
SHOW autovacuum_vacuum_scale_factor;
text
 autovacuum_vacuum_scale_factor
--------------------------------
 0.2
sql
-- 第 3 步:检查是否有长事务压制(同时看有没有正在跑的 worker)
SELECT pid, state, now() - xact_start AS xact_age, left(query, 50)
FROM pg_stat_activity
WHERE xact_start IS NOT NULL AND now() - xact_start > interval '1 minute'
ORDER BY xact_start LIMIT 5;
text
 pid  | state | xact_age | query
------+-------+----------+-------
(0 rows)

没有长事务,确认是阈值 + 成本延迟的问题。

sql
-- 第 4 步:修参数 + 立即手工清理一次
ALTER TABLE orders SET (
    autovacuum_vacuum_scale_factor  = 0.01,   -- 980万 × 1% ≈ 9.8万 死元组即触发
    autovacuum_analyze_scale_factor = 0.005,
    autovacuum_vacuum_cost_limit    = 1000,
    autovacuum_vacuum_cost_delay    = 2
);

VACUUM (ANALYZE, VERBOSE) orders;
sql
-- 第 5 步:验证新阈值会在合理时机触发
SELECT relname,
       n_dead_tup,
       round((50 + 0.01 * reltuples)::numeric) AS new_trigger_at,
       reloptions
FROM pg_stat_user_tables s
JOIN pg_class c ON c.oid = s.relid
WHERE relname = 'orders';
text
 relname | n_dead_tup | new_trigger_at |                                     reloptions
---------+------------+----------------+--------------------------------------------------------------------------------------
 orders  |          0 |          98204 | {autovacuum_vacuum_scale_factor=0.01,autovacuum_analyze_scale_factor=0.005,...}

场景二:表太多、worker 排不过来 ​

sql
-- 统计表数量,评估 3 个 worker 是否够用
SELECT count(*) AS tables_over_10mb,
       pg_size_pretty(sum(pg_total_relation_size(relid))) AS total_size
FROM pg_stat_user_tables
WHERE pg_total_relation_size(relid) > 10 * 1024 * 1024;
text
 tables_over_10mb | total_size
------------------+------------
              842 | 2841 GB

842 张 10MB 以上的表,只有 3 个 worker,每轮 naptime 只能处理极少数——清理必然滞后。

sql
-- 提高 worker 数量(PG 18 之前需要重启;PG 18 可在槽位范围内动态调整)
SHOW autovacuum_worker_slots;      -- 18+
SHOW autovacuum_max_workers;
text
 autovacuum_worker_slots
-------------------------
 16
 autovacuum_max_workers
------------------------
 3
sql
-- PostgreSQL 18:先确认槽位足够,然后动态调整,不必重启
ALTER SYSTEM SET autovacuum_max_workers = 8;
SELECT pg_reload_conf();
SHOW autovacuum_max_workers;
text
 设置
-------
 8

worker 不是越多越好

每个 worker 都会占用内存(主要是 maintenance_work_mem)、I/O 与 CPU。8 个 worker 同时清理可能把 I/O 打满。建议按「磁盘 IOPS / 单 worker 可承受 I/O」估算,并分批观察 pg_stat_progress_vacuum 与系统 iostat。

另一个更根本的方向是减少需要清理的表数量:归档冷表、用分区表按时间切分并 DETACH 历史分区。

场景三:防止 autovacuum 影响在线业务 ​

sql
-- 更温柔地清理「非核心但更新频繁」的表
ALTER TABLE session_tokens SET (
    autovacuum_vacuum_cost_limit = 100,   -- 从 1000 降到 100,占用更少 I/O
    autovacuum_vacuum_cost_delay = 20     -- 休眠更久
);

-- 结合 fillfactor,降低更新带来的页膨胀
ALTER TABLE session_tokens SET (fillfactor = 80);
sql
-- 观察它对 I/O 的实际影响(PG 16+)
SELECT backend_type, object, context,
       reads, writes, extends,
       read_bytes, write_bytes,
       round(reads::numeric / nullif(reads + writes, 0), 2) AS read_ratio
FROM pg_stat_io
WHERE backend_type = 'autovacuum worker';
text
   backend_type    |  object   |     context      | reads  | writes | read_bytes | write_bytes
-------------------+-----------+------------------+--------+--------+------------+-------------
 autovacuum worker | relation  | vacuum           | 184320 |  12041 |   1.4 GB   |    94 MB

场景四:纯插入表的冻结规划 ​

大量追加写入的表(日志、事件、埋点)几乎不产生死元组,但它们的元组都在变老,最终会触发防回卷清理。

sql
-- 看插入触发的清理是否已生效(PG 13+)
ALTER TABLE event_log SET (
    autovacuum_vacuum_insert_scale_factor = 0.05,
    autovacuum_freeze_max_age             = 100000000  -- 仅本表收紧到 1 亿,更早触发防回卷清理
);

表级 autovacuum_freeze_max_age 只能调小

该参数可以按表覆盖,但只允许设置为比全局值更小(官方原文:可以「reduced for individual tables」);设为比全局更大的值会被忽略。 全局值只能在服务器启动时设置,因此它偏大时,只能在个别敏感表上收紧,而不是把某张表放宽。

sql
-- 检查年龄
SELECT c.oid::regclass AS table_name,
       age(c.relfrozenxid) AS xid_age,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS size
FROM pg_class c
WHERE c.relname = 'event_log';
text
 table_name | xid_age  |  size
------------+----------+---------
 event_log  | 61204883 | 42 GB

autovacuum_freeze_max_age 只能在一定范围内调

该参数默认 2 亿。可以调大(上限约 2.1e9 的量级,需配合 vacuum_freeze_table_age 一起评估),但不能调到 1 亿以下——PostgreSQL 会把有效值静默提升到不低于 autovacuum_multixact_freeze_max_age 的 105%。且此参数修改需要重启。

注意事项 ​

1. autovacuum 是「默认开启」且「不能真正关闭」的

即使设 autovacuum = off,为防止事务 ID 回卷,系统仍会强制启动 worker。把它当开关关掉,只会让你失去日常清理,同时留下回卷风险。

sql
SHOW autovacuum;      -- 保持 on
SHOW track_counts;    -- 必须 on,否则 autovacuum 完全失效

2. track_counts 关闭会让 autovacuum 彻底失灵

它的判断数据全部来自累计统计。若 track_counts = off,worker 认为所有表都没有改动,只有防回卷清理还能触发。

sql
SHOW track_counts;   -- 必须为 on

3. 全局调参不如按表覆盖

全局降低 scale_factor 会让所有小表被过度清理(浪费资源),却仍不足以让最大的表满意。按表设阈值才是正解。

sql
-- 不推荐:一刀切调低全局比例
-- ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.01;

-- 推荐:只对热点大表覆盖
ALTER TABLE orders SET (autovacuum_vacuum_scale_factor = 0.01);

4. 提高 autovacuum_max_workers 前先看内存预算

每个 worker 可能申请一份 maintenance_work_mem(默认 64MB)。8 个 worker 就是 512MB 起的额外内存,且会叠加 work_mem 等开销。

sql
SHOW maintenance_work_mem;
SHOW autovacuum_max_workers;
-- 粗略估算:worker 数 × maintenance_work_mem ≤ 可用内存的 25%

5. 不要把 autovacuum_vacuum_cost_delay 设为 0 且不限制 cost_limit

在机械盘或共享存储上,这可能让 autovacuum 抢走全部 I/O 带宽,业务查询延迟飙升。合理组合是「延迟小 + 上限适中」。

sql
-- 激进但可控
ALTER TABLE orders SET (
    autovacuum_vacuum_cost_delay = 0,
    autovacuum_vacuum_cost_limit = 1000
);

6. 表级 autovacuum_enabled = off 要留「后手」

关闭后必须自己保证定期手工清理与冻结,否则迟早被强制防回卷清理「突袭」。

sql
-- 若确实要关闭,必须建立补偿机制
ALTER TABLE archive_2024 SET (autovacuum_enabled = off);
-- 并在调度系统中安排:VACUUM (ANALYZE) archive_2024;

7. 复制槽会阻止清理推进

长期 inactive 的复制槽会保留 WAL 并钉住老快照,导致清理无效、pg_wal 无限增长。PostgreSQL 18 新增了 idle_replication_slot_timeout 可自动失效空闲槽。

sql
SELECT slot_name, active, restart_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots
WHERE NOT active;

8. 分区表要关注「父表与子表」的统计口径

PG 18 起 VACUUM/ANALYZE 默认处理继承子表。分区表的清理是逐个分区进行的,因此分区粒度决定了单次清理的耗时——分区过少(单分区过大)会让清理变慢,过多则增加调度开销。

sql
-- 看各分区的大小分布,评估是否切分过粗
SELECT c.relname AS partition,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS size,
       s.n_dead_tup
FROM pg_class c
JOIN pg_stat_user_tables s ON s.relid = c.oid
WHERE c.relname LIKE 'orders_%'
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 10;

9. autovacuum 不清理索引膨胀

它只清理索引中的死项条目,不会重建或压缩索引页结构。索引碎片要靠 REINDEX。

sql
-- 索引膨胀检查
SELECT * FROM pgstatindex('orders_pkey');

10. 监控不要只看 last_autovacuum

它是「上次完成时间」。一个表可能 last_autovacuum 很新,但 n_dead_tup 依然很高(清理速度跟不上产生速度)。要结合比例一起看。

sql
SELECT relname, n_dead_tup, last_autovacuum,
       round(n_dead_tup * 100.0 / greatest(n_live_tup + n_dead_tup, 1), 2) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC LIMIT 10;

最佳实践 ​

1. 先把阈值算一遍,再决定调什么

text
拿到一张表,先算:
   触发阈值 = autovacuum_vacuum_threshold + scale_factor × 当前行数

如果结果远大于「几万行」→ 按表降低 scale_factor
如果表规模不大但清理仍滞后 → 查长事务 / worker 数量 / cost 限制
sql
-- 一次算出所有大表的当前触发阈值
SELECT relname,
       n_live_tup,
       n_dead_tup,
       round((current_setting('autovacuum_vacuum_threshold')::numeric
            + current_setting('autovacuum_vacuum_scale_factor')::numeric * n_live_tup)) AS trigger_at,
       last_autovacuum
FROM pg_stat_user_tables
WHERE n_live_tup > 100000
ORDER BY n_live_tup DESC
LIMIT 20;

2. 大表按表覆盖,小表保持默认

表规模建议配置
< 10 万行保持默认(比例阈值本身就落在合理区间)
10 万 ~ 100 万行scale_factor 降到 0.05,analyze_scale_factor 0.02
100 万 ~ 1000 万行scale_factor 0.02,analyze_scale_factor 0.01
> 1000 万行scale_factor 0.01 或更低,并按需放宽 cost_limit
sql
ALTER TABLE orders SET (
    autovacuum_vacuum_scale_factor  = 0.01,
    autovacuum_analyze_scale_factor = 0.005,
    autovacuum_vacuum_cost_limit    = 1000
);

3. 让清理跑得更快,而不只是更频繁

如果每次清理都被节流拖得很久,提高触发频率也追不上。同时调整 cost 参数,让单次清理迅速完成。

sql
-- 组合拳:更早触发 + 更快跑完
ALTER TABLE orders SET (
    autovacuum_vacuum_scale_factor = 0.01,
    autovacuum_vacuum_cost_limit   = 1000,
    autovacuum_vacuum_cost_delay   = 2
);

4. 把「成本延迟」和「磁盘类型」挂钩

存储建议 cost_delay建议 cost_limit
机械盘 / 网络存储20ms(甚至更高)200(默认)
SATA SSD2~10ms200~500
本地 NVMe0~2ms1000~2000

5. 强制记录 autovacuum 日志

sql
ALTER SYSTEM SET log_autovacuum_min_duration = 1000;
SELECT pg_reload_conf();

6. 把「事务时长」纳入最高优先级监控

绝大多数「清理无效」的根因都是长事务。监控最老事务时长,比监控 n_dead_tup 更早发现问题。

sql
SELECT max(now() - xact_start) AS oldest_xact
FROM pg_stat_activity
WHERE xact_start IS NOT NULL;

7. 常规清理与防回卷清理要分开规划

类型触发者特点运维策略
常规清理阈值(死元组/插入)受 cost 限制,可控调参数让其及时跑完
分析阈值(修改数)轻量,主要读保证每天至少一轮
防回卷清理autovacuum_freeze_max_age忽略节流,I/O 冲击大提前干预,别等它触发
sql
-- 提前发现「即将触发防回卷」的表
SELECT c.oid::regclass AS table_name, age(c.relfrozenxid) AS xid_age
FROM pg_class c
WHERE c.relkind = 'r' AND age(c.relfrozenxid) > 150000000
ORDER BY age(c.relfrozenxid) DESC;

8. maintenance_work_mem 给够,但别无脑给大

它决定清理与索引重建的单次内存上限。默认 64MB 对中小库够用,大表可提到 256MB~1GB;但要注意「每个 worker 各自一份」,worker 数 × maintenance_work_mem 是实际内存压力。

sql
ALTER SYSTEM SET maintenance_work_mem = '256MB';
SELECT pg_reload_conf();

9. 定期复核表级参数,避免「参数漂移」

表级参数会随表一起演进(例如表被归档后不需要那么激进的清理)。建议建一张「表级参数清单」,或者定期扫一遍:

sql
-- 列出所有设置了非默认 autovacuum 参数的表
SELECT c.oid::regclass AS table_name,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS size,
       c.reloptions
FROM pg_class c
WHERE c.relkind = 'r'
  AND c.reloptions IS NOT NULL
  AND array_to_string(c.reloptions, ',') LIKE '%autovacuum%'
ORDER BY pg_total_relation_size(c.oid) DESC;

10. 把 autovacuum 调优当作「一次性投入 + 定期复核」

正确的做法不是每天盯着它,而是:初始化时按表规模设好参数 → 建立告警 → 每季度复核一次。

text
季度复核清单
├── 最大的 20 张表的 n_dead_tup 比例是否长期 < 10%
├── 有没有 age(relfrozenxid) > 1.5 亿 的表
├── n_mod_since_analyze / n_live_tup 是否长期 > 20%
├── 是否出现新的超大表(需要新加按表覆盖)
├── 是否有遗留的 inactive 复制槽 / 预备事务
└── 日志中是否有 "dead but not yet removable" 频繁出现

下一节

继续学习:表膨胀检测与处理

参考链接 ​