AutoVacuum 配置
生产环境里,没有人会记得每天手工跑 VACUUM。PostgreSQL 用一个后台守护进程把这个动作自动化,它就是 autovacuum(自动清理)。它会按「死元组累积到多少」的阈值自动触发清理与统计收集,并在事务 ID 濒临回卷时强制介入。
问题是:autovacuum 的默认参数是为小库设计的。在一张亿级行的大表上,默认阈值意味着要积累两千万个死元组才会清理一次——那时表已经膨胀好几倍,单次清理也变成一场灾难。所以「autovacuum 跟不上」几乎总是配置问题,而不是它不够努力。
本节从参数配置与调优的角度讲清 autovacuum:进程架构、触发条件怎么算、每个参数该设多少、按表覆盖怎么做、怎么监控、为什么经常「跟不上」、以及如何排障。进程本身的调度机制可参考架构章节。
前置知识
阅读本节前,建议先了解:VACUUM FULL 与重建
基础概念
进程架构:launcher 与 worker
autovacuum 不是一个进程,而是一个调度器 + 若干工作进程:
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/ANALYZE | autovacuum_max_workers |
| 工作进程槽位 | 预留的后端槽位,允许运行时上调 worker 数 | autovacuum_worker_slots(18+) |
| 统计收集 | worker 判断依据的数据来源 | track_counts(必须为 on) |
每个 worker 最多同时处理一张表。3 个 worker 意味着最多 3 张表在并行清理——这是理解「为什么大库排不过来」的关键。
-- 看看当前有几个 autovacuum 进程在跑
SELECT pid, backend_type, state, query
FROM pg_stat_activity
WHERE backend_type = 'autovacuum worker';触发条件:一张表什么时候会被清理
autovacuum 用阈值 + 比例的组合公式判断,而不是「固定死元组数」:
触发自动 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_threshold | 50 | 固定基数(死元组) |
autovacuum_vacuum_scale_factor | 0.2 | 表大小的百分比 |
autovacuum_vacuum_insert_threshold | 1000(13+) | 纯 INSERT 触发的固定基数 |
autovacuum_vacuum_insert_scale_factor | 0.2(13+) | 纯 INSERT 触发的百分比 |
autovacuum_analyze_threshold | 50 | 固定基数(被修改元组) |
autovacuum_analyze_scale_factor | 0.1 | 表大小的百分比 |
代入不同规模的表,看阈值变化:
表行数 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 factor | 0.2 | 0.1 |
| 只看插入 | 仅 PG 13+ 支持 | 支持(修改即计数) |
| 不做会怎样 | 表膨胀、回卷风险 | 执行计划选错、变慢 |
一个常见误区是「表不膨胀就不用管 autovacuum」。实际上统计信息过期导致的全表扫描,往往比膨胀更早影响业务。所以两套阈值都要调。
语法与用法
全局参数一览
在 postgresql.conf 或通过 ALTER SYSTEM 设置:
| 参数 | 默认值 | 作用 | 生效方式 |
|---|---|---|---|
autovacuum | on | 总开关(关闭后仍会为防回卷启动 worker) | 重启 |
autovacuum_max_workers | 3 | worker 数量上限 | 重启(18+ 可在 autovacuum_worker_slots 范围内动态调整) |
autovacuum_worker_slots | 16(18+) | 预留的后端槽位数,允许运行时上调 worker 数 | 重启 |
autovacuum_naptime | 1min | launcher 唤醒间隔 | 重载 |
autovacuum_vacuum_threshold | 50 | 死元组固定阈值 | 重载 |
autovacuum_vacuum_scale_factor | 0.2 | 死元组比例阈值 | 重载 |
autovacuum_vacuum_insert_threshold | 1000(13+) | 插入固定阈值 | 重载 |
autovacuum_vacuum_insert_scale_factor | 0.2(13+) | 插入比例阈值 | 重载 |
autovacuum_vacuum_max_threshold | 100000000(18+) | 死元组绝对上限,超过必定触发 | 重载 |
autovacuum_analyze_threshold | 50 | 修改元组固定阈值 | 重载 |
autovacuum_analyze_scale_factor | 0.1 | 修改元组比例阈值 | 重载 |
autovacuum_vacuum_cost_delay | 2ms(12+) | worker 节流休眠时长;-1 表示沿用 vacuum_cost_delay | 重载 |
autovacuum_vacuum_cost_limit | -1 | worker 成本上限;-1 表示沿用 vacuum_cost_limit(默认 200) | 重载 |
autovacuum_freeze_max_age | 200000000 | 强制防回卷清理的年龄上限 | 重启 |
autovacuum_multixact_freeze_max_age | 400000000 | 多事务 ID 的对应上限 | 重启 |
autovacuum_vacuum_scale_factor | — | 见上 | — |
log_autovacuum_min_duration | -1(不记录) | 超过该毫秒数的 autovacuum 记入日志;0 表示全记 | 重载 |
track_counts | on | 统计计数开关,必须为 on | 重启 |
PostgreSQL 18 的两个新参数
autovacuum_vacuum_max_threshold:给死元组数设一个绝对上限。超过它时无论如何都触发清理,弥补了「百分比阈值在大表上永远不触发」的缺陷。默认 1 亿。
autovacuum_worker_slots:预留 autovacuum 工作进程可用的后端槽位(默认 16)。设置之后,autovacuum_max_workers 可以在运行时上调到该值以内,不必重启。
-- 先留出槽位(需重启),之后即可动态调整 worker 数
ALTER SYSTEM SET autovacuum_worker_slots = 16;
-- 重启后:
ALTER SYSTEM SET autovacuum_max_workers = 8;
SELECT pg_reload_conf();
SHOW autovacuum_max_workers;按表覆盖(最重要的一节)
绝大多数字段都可以在表级覆盖,这是解决「某张大表不清理」的首选手段,因为它比调全局参数精准得多:
ALTER TABLE 表名 SET (
参数 = 值
);也可以在建表时写入:
CREATE TABLE orders (
...
) WITH (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_analyze_scale_factor = 0.005
);可用的表级参数:
| 表级参数 | 等价全局参数 | 说明 |
|---|---|---|
autovacuum_enabled | autovacuum | 是否对该表启用自动清理 |
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_age | vacuum_freeze_min_age | 冻结最小年龄 |
autovacuum_freeze_max_age | 同名 | 防回卷年龄上限 |
autovacuum_freeze_table_age | vacuum_freeze_table_age | 触发全表冻结的年龄 |
autovacuum_multixact_freeze_max_age | 同名 | 多事务 ID 上限 |
log_autovacuum_min_duration | 同名 | 该表的日志阈值 |
vacuum_truncate | vacuum_truncate(18+) | 是否允许截断尾部空页 |
vacuum_index_cleanup | INDEX_CLEANUP | 是否清理索引死项 |
查看某张表实际生效的参数:
SELECT relname, reloptions
FROM pg_class
WHERE relname = 'orders'; relname | reloptions
---------+----------------------------------------------------------------------------------
orders | {autovacuum_vacuum_scale_factor=0.01,autovacuum_analyze_scale_factor=0.005}按表调优的典型配方
不同负载的表,参数取向完全不同:
-- ① 高频更新的大表(订单、计数器、状态表)
-- 关键:让阈值落在几万行量级,且清理要跑得快
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 让它更温柔,而不是关掉它。
临时全局压制的写法
有时需要在批量导入期间临时放宽节奏(而不是关闭):
-- 批量导入窗口:暂停 autovacuum 的调度(仅影响常规清理)
ALTER SYSTEM SET autovacuum = off;
SELECT pg_reload_conf();
-- 导入完成后立即恢复并主动清理
ALTER SYSTEM SET autovacuum = on;
SELECT pg_reload_conf();
VACUUM (ANALYZE) big_import_table;-- 更温和的替代:只让它变慢,不关闭
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 挤掉。机制是一个「成本计数器」:
每个 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:
每处理约 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——所有活跃事务中最老的快照边界。
时间轴 →
事务 A 开始(快照固定在 T1)━━━━━━━━━━━━━━━━━━━━━━━━━ A 提交
│
在 T1 之后被 UPDATE 掉的元组
对 A 来说「仍需要看到」→ 不能回收
│
VACUUM 跑了 N 次,每次都报告:
"N dead row versions cannot be removed yet, oldest xmin: ..."所以排查 autovacuum 无效时,第一件事是查长事务:
-- 按事务开始时间排序,找出最老的事务
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; 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:
-- 复制槽: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:
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 的资源占比:
-- 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; relname | total_vacuum_time | total_autovacuum_time | total_analyze_time | total_autoanalyze_time
---------+-------------------+-----------------------+--------------------+------------------------
orders | 0 | 1284320 | 0 | 38410
products| 0 | 42180 | 0 | 9120观察正在进行的清理
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); 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 可以判断是自动还是手工:
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 做防回卷清理。日志里会出现:
LOG: automatic vacuum of table "demo.public.orders": index scans: 1
...
to prevent wraparound-- 查看年龄最大的表,判断是否需要人工介入
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; 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 冲击,甚至阻塞业务。因此不能把回卷防护当作日常清理手段——它只是兜底。
如果发现表已经逼近上限,应该主动处理:
-- 在低峰期手工冻结
VACUUM (FREEZE, ANALYZE, VERBOSE) orders;细节见 冻结事务 ID(FREEZE)。
监控与告警建议
一套最小可用的巡检 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;-- ② 统计信息过期的表(影响执行计划)
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;-- ③ 事务 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;-- ④ 库级年龄(回卷风险的总览)
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 秒的清理」:
ALTER SYSTEM SET log_autovacuum_min_duration = 1000; -- 单位毫秒
SELECT pg_reload_conf();日志形态:
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 好像没在工作」
-- 第 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; 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+08orders 有 980 万活元组、483 万死元组,而上次自动清理在 27 天前——典型的大表阈值过高问题。
-- 第 2 步:确认阈值——980 万行 × 0.2 = 约 196 万
SHOW autovacuum_vacuum_scale_factor; autovacuum_vacuum_scale_factor
--------------------------------
0.2-- 第 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; pid | state | xact_age | query
------+-------+----------+-------
(0 rows)没有长事务,确认是阈值 + 成本延迟的问题。
-- 第 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;-- 第 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'; relname | n_dead_tup | new_trigger_at | reloptions
---------+------------+----------------+--------------------------------------------------------------------------------------
orders | 0 | 98204 | {autovacuum_vacuum_scale_factor=0.01,autovacuum_analyze_scale_factor=0.005,...}场景二:表太多、worker 排不过来
-- 统计表数量,评估 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; tables_over_10mb | total_size
------------------+------------
842 | 2841 GB842 张 10MB 以上的表,只有 3 个 worker,每轮 naptime 只能处理极少数——清理必然滞后。
-- 提高 worker 数量(PG 18 之前需要重启;PG 18 可在槽位范围内动态调整)
SHOW autovacuum_worker_slots; -- 18+
SHOW autovacuum_max_workers; autovacuum_worker_slots
-------------------------
16
autovacuum_max_workers
------------------------
3-- PostgreSQL 18:先确认槽位足够,然后动态调整,不必重启
ALTER SYSTEM SET autovacuum_max_workers = 8;
SELECT pg_reload_conf();
SHOW autovacuum_max_workers; 设置
-------
8worker 不是越多越好
每个 worker 都会占用内存(主要是 maintenance_work_mem)、I/O 与 CPU。8 个 worker 同时清理可能把 I/O 打满。建议按「磁盘 IOPS / 单 worker 可承受 I/O」估算,并分批观察 pg_stat_progress_vacuum 与系统 iostat。
另一个更根本的方向是减少需要清理的表数量:归档冷表、用分区表按时间切分并 DETACH 历史分区。
场景三:防止 autovacuum 影响在线业务
-- 更温柔地清理「非核心但更新频繁」的表
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);-- 观察它对 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'; backend_type | object | context | reads | writes | read_bytes | write_bytes
-------------------+-----------+------------------+--------+--------+------------+-------------
autovacuum worker | relation | vacuum | 184320 | 12041 | 1.4 GB | 94 MB场景四:纯插入表的冻结规划
大量追加写入的表(日志、事件、埋点)几乎不产生死元组,但它们的元组都在变老,最终会触发防回卷清理。
-- 看插入触发的清理是否已生效(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」);设为比全局更大的值会被忽略。 全局值只能在服务器启动时设置,因此它偏大时,只能在个别敏感表上收紧,而不是把某张表放宽。
-- 检查年龄
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'; table_name | xid_age | size
------------+----------+---------
event_log | 61204883 | 42 GBautovacuum_freeze_max_age 只能在一定范围内调
该参数默认 2 亿。可以调大(上限约 2.1e9 的量级,需配合 vacuum_freeze_table_age 一起评估),但不能调到 1 亿以下——PostgreSQL 会把有效值静默提升到不低于 autovacuum_multixact_freeze_max_age 的 105%。且此参数修改需要重启。
注意事项
1. autovacuum 是「默认开启」且「不能真正关闭」的
即使设 autovacuum = off,为防止事务 ID 回卷,系统仍会强制启动 worker。把它当开关关掉,只会让你失去日常清理,同时留下回卷风险。
SHOW autovacuum; -- 保持 on
SHOW track_counts; -- 必须 on,否则 autovacuum 完全失效2. track_counts 关闭会让 autovacuum 彻底失灵
它的判断数据全部来自累计统计。若 track_counts = off,worker 认为所有表都没有改动,只有防回卷清理还能触发。
SHOW track_counts; -- 必须为 on3. 全局调参不如按表覆盖
全局降低 scale_factor 会让所有小表被过度清理(浪费资源),却仍不足以让最大的表满意。按表设阈值才是正解。
-- 不推荐:一刀切调低全局比例
-- 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 等开销。
SHOW maintenance_work_mem;
SHOW autovacuum_max_workers;
-- 粗略估算:worker 数 × maintenance_work_mem ≤ 可用内存的 25%5. 不要把 autovacuum_vacuum_cost_delay 设为 0 且不限制 cost_limit
在机械盘或共享存储上,这可能让 autovacuum 抢走全部 I/O 带宽,业务查询延迟飙升。合理组合是「延迟小 + 上限适中」。
-- 激进但可控
ALTER TABLE orders SET (
autovacuum_vacuum_cost_delay = 0,
autovacuum_vacuum_cost_limit = 1000
);6. 表级 autovacuum_enabled = off 要留「后手」
关闭后必须自己保证定期手工清理与冻结,否则迟早被强制防回卷清理「突袭」。
-- 若确实要关闭,必须建立补偿机制
ALTER TABLE archive_2024 SET (autovacuum_enabled = off);
-- 并在调度系统中安排:VACUUM (ANALYZE) archive_2024;7. 复制槽会阻止清理推进
长期 inactive 的复制槽会保留 WAL 并钉住老快照,导致清理无效、pg_wal 无限增长。PostgreSQL 18 新增了 idle_replication_slot_timeout 可自动失效空闲槽。
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 默认处理继承子表。分区表的清理是逐个分区进行的,因此分区粒度决定了单次清理的耗时——分区过少(单分区过大)会让清理变慢,过多则增加调度开销。
-- 看各分区的大小分布,评估是否切分过粗
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。
-- 索引膨胀检查
SELECT * FROM pgstatindex('orders_pkey');10. 监控不要只看 last_autovacuum
它是「上次完成时间」。一个表可能 last_autovacuum 很新,但 n_dead_tup 依然很高(清理速度跟不上产生速度)。要结合比例一起看。
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. 先把阈值算一遍,再决定调什么
拿到一张表,先算:
触发阈值 = autovacuum_vacuum_threshold + scale_factor × 当前行数
如果结果远大于「几万行」→ 按表降低 scale_factor
如果表规模不大但清理仍滞后 → 查长事务 / worker 数量 / cost 限制-- 一次算出所有大表的当前触发阈值
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 |
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_analyze_scale_factor = 0.005,
autovacuum_vacuum_cost_limit = 1000
);3. 让清理跑得更快,而不只是更频繁
如果每次清理都被节流拖得很久,提高触发频率也追不上。同时调整 cost 参数,让单次清理迅速完成。
-- 组合拳:更早触发 + 更快跑完
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 SSD | 2~10ms | 200~500 |
| 本地 NVMe | 0~2ms | 1000~2000 |
5. 强制记录 autovacuum 日志
ALTER SYSTEM SET log_autovacuum_min_duration = 1000;
SELECT pg_reload_conf();6. 把「事务时长」纳入最高优先级监控
绝大多数「清理无效」的根因都是长事务。监控最老事务时长,比监控 n_dead_tup 更早发现问题。
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 冲击大 | 提前干预,别等它触发 |
-- 提前发现「即将触发防回卷」的表
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 是实际内存压力。
ALTER SYSTEM SET maintenance_work_mem = '256MB';
SELECT pg_reload_conf();9. 定期复核表级参数,避免「参数漂移」
表级参数会随表一起演进(例如表被归档后不需要那么激进的清理)。建议建一张「表级参数清单」,或者定期扫一遍:
-- 列出所有设置了非默认 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 调优当作「一次性投入 + 定期复核」
正确的做法不是每天盯着它,而是:初始化时按表规模设好参数 → 建立告警 → 每季度复核一次。
季度复核清单
├── 最大的 20 张表的 n_dead_tup 比例是否长期 < 10%
├── 有没有 age(relfrozenxid) > 1.5 亿 的表
├── n_mod_since_analyze / n_live_tup 是否长期 > 20%
├── 是否出现新的超大表(需要新加按表覆盖)
├── 是否有遗留的 inactive 复制槽 / 预备事务
└── 日志中是否有 "dead but not yet removable" 频繁出现下一节
继续学习:表膨胀检测与处理