Autovacuum(自动清理进程)
Autovacuum(自动清理)由 Autovacuum Launcher(自动清理启动进程)和若干 Autovacuum Worker(自动清理工作进程)组成,是 PostgreSQL 中唯一由统计信息驱动、自动回收死元组并更新统计信息的机制。本节将讲解它的进程结构、调度逻辑、成本延迟节流、以及如何监控与调优。
前置知识
阅读本节前,建议先了解:WAL Writer(预写日志写入进程)
基础概念
| 术语 | 英文 | 说明 |
|---|---|---|
| 自动清理启动进程 | Autovacuum Launcher | 常驻进程(每实例 1 个),负责收集需求并调度 worker |
| 自动清理工作进程 | Autovacuum Worker | 按需派生(最大 autovacuum_max_workers 个),执行具体表的 VACUUM / ANALYZE |
| 死元组 | Dead Tuple | 已被删除或已被更新替代、且不再对任何事务可见的元组版本 |
| 膨胀 | Bloat | 表或索引中死元组占用空间过多,导致实际文件远超有效数据所需 |
| 事务 ID 环绕 | XID Wraparound | XID 用尽(32 位)导致的灾难性问题,靠冻结(Freeze)避免 |
| 冻结 | Freeze | 把元组的 xmin 标记为「永久可见」,使其不再受 XID 比较约束 |
| 冻结映射 | FrozenMap(pg_visibility) | 可见性映射(VM)中描述「该页所有元组均已冻结」的位 |
| 成本延迟 | Cost-based Vacuum Delay | 累计达到 autovacuum_vacuum_cost_limit 后主动睡眠,控制 I/O 冲击 |
| 统计信息收集器 | Statistics Collector | PG 15 前为独立进程;PG 15+ 改为共享内存统计 |
| 表级参数 | Storage Parameters | 可在表上单独覆盖 autovacuum 全局阈值(ALTER TABLE ... SET) |
Autovacuum 不是「可选优化」,而是必需机制
很多团队习惯性关闭 autovacuum(认为它「乱抢 I/O」),这是极其危险的做法。它同时承担两项不可替代的职责:
- 回收死元组空间(VACUUM)——否则表和索引会无限膨胀。
- 推进冻结、防止 XID 环绕(VACUUM FREEZE)——否则数据库会在达到
autovacuum_freeze_max_age后被迫停机进行冻结,或者在极端情况下数据损坏。
正确做法是调优它(降低单表冲击、提高覆盖度),而不是关闭它。若确实需要避开业务高峰,应该用 autovacuum 的表级参数与调度策略,而不是全局关闭。
进程结构
Autovacuum 是「1 个 launcher + N 个 worker」的两级结构,这与「常驻单进程」的 bgwriter / walwriter 不同:
┌──────────────────────────────┐
│ Autovacuum Launcher │
│ (每实例 1 个,常驻) │
│ │
│ ① 定期收集「哪些库需要清理」 │
│ ② 从共享内存需求队列取任务 │
│ ③ 派生 worker(最多 N 个) │
└───────┬──────────────────────┘
│ fork()
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ AV Worker 1 │ │ AV Worker 2 │ │ AV Worker 3 │
│ (处理 db A)│ │ (处理 db B)│ │ (处理 db A)│
│ VACUUM 表 X │ │ VACUUM 表 Y │ │ ANALYZE 表Z │
└─────────────┘ └─────────────┘ └─────────────┘
(最多 autovacuum_max_workers 个,默认 3)-- 查看各类后台进程(PG 10+ 有 backend_type 列)
SELECT
backend_type,
count(*) AS cnt,
max(backend_start) AS latest_start
FROM pg_stat_activity
GROUP BY backend_type
ORDER BY cnt DESC; backend_type | cnt | latest_start
---------------------+-----+----------------------------
client backend | 12 | 2026-09-22 09:14:02.113+08
autovacuum worker | 2 | 2026-09-22 09:12:48.901+08
autovacuum launcher | 1 | 2026-09-22 08:00:11.204+08
checkpointer | 1 | 2026-09-22 08:00:11.203+08
background writer | 1 | 2026-09-22 08:00:11.202+08
walwriter | 1 | 2026-09-22 08:00:11.201+08为什么 launcher 与 worker 要分开
Worker 是「用完即弃」的:每处理完一个库(或一轮任务)就退出,由 launcher 重新派生。这样做的好处是:
- 隔离性:worker 崩溃只影响自己,launcher 可以重新拉一个。
- 动态伸缩:空闲时 worker 数为 0,不占内存与 CPU;繁忙时最多扩到
autovacuum_max_workers。 - 内存卫生:worker 长期运行会积累 catalog cache 等内存,定期重建可避免泄漏累积。
因此你在 pg_stat_activity 中会看到 worker 的 backend_start 不断变化,而 launcher 的 backend_start 与实例启动时间一致。
核心机制
Launcher 的调度循环
Launcher 每隔一段时间(由 autovacuum_naptime 决定,默认 1min)醒来一次,做以下事情:
① 读取本实例的数据库列表
② 对每个数据库,判断「是否需要清理」:
- 该库最老的表,死元组数是否超过 autovacuum_vacuum_threshold
- 该库最老的表,是否接近 autovacuum_freeze_max_age
③ 把需要处理的库放入「需求列表」
④ 若有空闲 worker 槽位,从列表取一个库,派生一个 worker 去处理
⑤ 继续 naptime 睡眠关键点:launcher 是按「数据库」粒度分派的,worker 是按「表」粒度工作的。一个 worker 连到某个库后,会遍历该库中所有「需要清理」的表,逐张处理。
naptime = 1min,max_workers = 3 时的典型节奏:
T+0min launcher 醒来 → 发现 db1、db2 需要清理 → 派生 worker1(db1)、worker2(db2)
T+0min worker1 开始处理 db1 中最需要清理的表
T+1min launcher 醒来 → 仍有空闲槽位(worker3 未用)→ 派生 worker3(db2)
T+5min worker1 处理完 db1 的所有待清理表 → 退出,槽位释放
T+6min launcher 醒来 → 派生 worker4(db1) 继续处理autovacuum_naptime 的语义是「每个数据库的检查间隔」:当有 D 个数据库时,launcher 的醒来频率约为 max(naptime / D, 1s),以保证每个库都在 naptime 内被检查一次。
autovacuum_naptime 调小不等于清理更快
调小 naptime 只是「更勤快地检查是否该清理」,而不是「清理得更快」。真正的清理速度受限于:
autovacuum_max_workers(并行度上限)autovacuum_vacuum_cost_limit/autovacuum_vacuum_cost_delay(每个 worker 的 I/O 节流)- 表的大小与索引数量
因此「清理跟不上」的正解通常是提高 max_workers 与放宽 cost_limit,而不是把 naptime 调到 1s。
触发的判定公式
自动 VACUUM 与自动 ANALYZE 的触发阈值公式:
触发自动 VACUUM:
死元组数 > autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor × 表当前元组数
触发自动 ANALYZE:
被修改元组数 > autovacuum_analyze_threshold
+ autovacuum_analyze_scale_factor × 表当前元组数
触发表级 ANALYZE(用于优化器统计,与 autovacuum 独立)可加:
INSERT 元组数 > autovacuum_vacuum_insert_threshold
+ autovacuum_vacuum_insert_scale_factor × 表当前元组数 -- PG 13+默认值与代入计算:
| 参数 | 默认值 | 含义 |
|---|---|---|
autovacuum_vacuum_threshold | 50 | 固定基数(元组数) |
autovacuum_vacuum_scale_factor | 0.2 | 表大小的百分比 |
autovacuum_analyze_threshold | 50 | 固定基数 |
autovacuum_analyze_scale_factor | 0.1 | 表大小的百分比 |
autovacuum_vacuum_insert_threshold | 1000(PG 13+) | 纯 INSERT 触发的基数 |
autovacuum_vacuum_insert_scale_factor | 0.2(PG 13+) | 纯 INSERT 触发的百分比 |
假设表有 1,000,000 行(均为活元组),默认参数下:
VACUUM 阈值 = 50 + 0.2 × 1,000,000 = 200,050 个死元组
ANALYZE 阈值 = 50 + 0.1 × 1,000,000 = 100,050 个被修改元组这解释了一个经典问题:大表 20% 的阈值太高。
表有 100,000,000 行时:
VACUUM 阈值 = 50 + 0.2 × 100,000,000 = 20,000,050 个死元组
也就是说,要积累两千万个死元组才会触发一次 VACUUM。
在这期间表可能已经膨胀到原来的数倍,而且单次 VACUUM 要处理两千万个死元组,
耗时极长、冲击极大 —— 形成「越不清理越难清理」的恶性循环。大表必须用表级参数覆盖
对大表(百万行以上),必须用 ALTER TABLE ... SET (...) 覆盖全局阈值:
ALTER TABLE t_big SET (
autovacuum_vacuum_scale_factor = 0.01, -- 从 20% 降到 1%
autovacuum_analyze_scale_factor = 0.005, -- 从 10% 降到 0.5%
autovacuum_vacuum_cost_delay = 2 -- 从 2ms 降到 2ms(可按盘调整)
);经验法则:让阈值落在「几万到几十万行」这个量级。这样每次清理的工作量可控,且不会让膨胀累积。
成本延迟节流(Cost-based Vacuum Delay)
Autovacuum 最被诟病的「会拖慢业务」,本质上是因为它默认对 I/O 设了很严格的节流——所以它其实已经很「温柔」了,问题往往出在「太温柔导致清理不完」。
节流机制基于一套成本模型:
| 操作 | 成本(默认) | 参数 |
|---|---|---|
| 从共享缓冲区命中读一页 | 1 | vacuum_cost_page_hit |
| 从磁盘读一页(未命中缓冲区) | 2 | vacuum_cost_page_miss(PG 17+ 默认由 10 降为 2) |
| 写一个脏页 | 20 | vacuum_cost_page_dirty |
节流逻辑(每个 autovacuum worker 独立累计):
每完成一次页面访问 → 累加对应成本
当累计成本 ≥ autovacuum_vacuum_cost_limit(默认 200)
→ 睡眠 autovacuum_vacuum_cost_delay(默认 2ms)
→ 把累计成本清零,继续
因此每个 worker 的「理论最大 I/O 速率」约为:
200 成本 / (2ms 睡眠 + 实际工作时间) ≈ 每 2ms 处理 200 成本的页默认参数下,每个 worker 大约只能利用 几 MB/s 到十几 MB/s 的 I/O 带宽。在 SSD 上这远远用不满设备能力。
-- 查看当前节流参数
SELECT name, setting, unit, context
FROM pg_settings
WHERE name IN (
'autovacuum_vacuum_cost_delay',
'autovacuum_vacuum_cost_limit',
'vacuum_cost_page_hit',
'vacuum_cost_page_miss',
'vacuum_cost_page_dirty'
)
ORDER BY name; name | setting | unit | context
-----------------------------+---------+------+-----------
autovacuum_vacuum_cost_delay| 2 | ms | sighup
autovacuum_vacuum_cost_limit| -1 | | sighup
vacuum_cost_page_dirty | 20 | | user
vacuum_cost_page_hit | 1 | | user
vacuum_cost_page_miss | 2 | | userautovacuum_vacuum_cost_limit = -1 的含义
-1 表示「继承 vacuum_cost_limit 的值」(默认 200)。这是 PG 里少见的「继承式默认值」。
另外要注意一个历史遗留行为:autovacuum_vacuum_cost_limit 是所有 autovacuum worker 共享的总限额(而不是每个 worker 独立)——它会被平均分配给当前活跃的 worker。所以:
若 autovacuum_vacuum_cost_limit = 200,有 3 个 worker 在跑:
每个 worker 实际可用成本 ≈ 200 / 3 ≈ 66
这就是「增加 autovacuum_max_workers 后单个 worker 反而更慢」的原因:
总限额被更多 worker 分薄了。
因此提高 max_workers 时,通常需要同步提高 cost_limit
(经验做法:cost_limit ≈ 200 × max_workers)。PG 17 起,vacuum_cost_page_miss 的默认值从 10 降到了 2,使成本模型更贴近 SSD 的实际特性,间接提升了 autovacuum 的有效速度。
VACUUM 与 ANALYZE 的分工
Worker 会执行两类任务,它们的目标完全不同:
| 维度 | VACUUM | ANALYZE |
|---|---|---|
| 目标 | 回收死元组空间、推进冻结、更新可见性映射 | 采样表内容、生成优化器所需的统计信息 |
| 是否改数据文件 | 是(标记空间可复用,通常不缩短文件) | 否(只读采样) |
| 是否拿排他锁 | 否(SHARE UPDATE EXCLUSIVE,不阻塞读写) | 否(SHARE UPDATE EXCLUSIVE) |
| 主要产物 | FSM 更新、VM 更新、relfrozenxid 推进 | pg_statistic / pg_stats 更新 |
| 相关统计视图 | pg_stat_all_tables.n_dead_tup、last_autovacuum | pg_stat_all_tables.last_autoanalyze |
-- 查看哪些表最需要被处理:按死元组占比排序
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_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_all_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC
LIMIT 10; schemaname | relname | n_live_tup | n_dead_tup | dead_pct | last_autovacuum
------------+-----------------------+------------+------------+----------+----------------------------
public | t_order | 4821033 | 1842122 | 27.64 | 2026-09-22 09:02:41.113+08
public | t_order_item | 12840921 | 3120984 | 19.55 | 2026-09-22 09:08:12.882+08
public | t_user_session | 212044 | 182331 | 46.23 | 2026-09-22 08:41:02.004+08看 dead_pct 而不是绝对数
n_dead_tup 的绝对值不能说明问题:100 万死元组在 1000 万行的表里是 10%(可接受),在 20 万行的表里是 500%(严重)。
重点关注两类表:
dead_pct持续 > 20%:说明 autovacuum 跟不上或阈值过高。- 小表但死元组极多(如
t_user_session):这类表通常是「高频 UPDATE / DELETE 的会话表」,应该用更激进的表级参数,甚至考虑改用UNLOGGED表或外部缓存。
详细说明
长事务如何「压制」autovacuum
这是生产环境中最常见的隐形杀手。VACUUM 只能回收对所有活跃事务与快照都不可见的死元组。如果存在一个长事务(或长 idle in transaction 会话、或长命的复制槽),它的快照会「钉住」某个 XID,使该 XID 之后产生的所有死元组都不能被回收。
会话 A:BEGIN; SELECT * FROM t_x; ← 不提交,快照固定在 XID = 1000
然后去吃饭(idle in transaction)
期间 t_x 被 UPDATE 了 100 万次 → 产生 100 万死元组
但这些死元组对「XID=1000 的快照」仍可能可见 → VACUUM 无法回收
会话 A 挂 2 小时后断开 → 快照释放 → 下一个 VACUUM 才能一次性回收 100 万死元组
(单次超大清理,I/O 冲击大,且期间表已膨胀)诊断手段:查看 pg_stat_activity 中的长事务,以及后端进程持有的 backend_xmin。
-- 找出当前所有后端持有的最老事务快照(backend_xmin 越小越危险)
SELECT
pid,
usename,
application_name,
state,
backend_xid,
backend_xmin,
now() - xact_start AS xact_age,
now() - state_change AS state_age,
left(query, 60) AS query_snippet
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY backend_xmin::text::bigint
LIMIT 10; pid | usename | application_name | state | backend_xmin | xact_age | state_age
-------+---------+------------------+---------------------+--------------+--------------+-----------
18422 | app | order-service | idle in transaction | 18421903 | 02:14:38.221 | 02:14:31.902
19701 | report | bi-tool | active | 18491102 | 00:18:02.114 | 00:00:01.220-- 查看某个会话是否阻塞了 autovacuum 回收
SELECT pg_blocking_pids(18422);-- 计算「最老快照」导致的元组积压量(需 pg_stat_activity 有 backend_xmin)
SELECT
min(backend_xmin::text::bigint) AS oldest_xmin,
age(min(backend_xmin::text::bigint::xid)) AS oldest_xmin_age
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL;复制槽比长事务更隐蔽
不活跃的逻辑复制槽(Inactive Logical Replication Slot) 会永久保留 catalog_xmin / restart_lsn,导致 autovacuum 永远无法清理对应的死元组与 WAL。这类问题往往在几周后才通过磁盘爆满被发现。
必须监控:
SELECT
slot_name,
slot_type,
active,
restart_lsn,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal,
xmin,
catalog_xmin
FROM pg_replication_slots;只要出现 active = false 且 retained_wal 持续增长,就应该立即判断是否需要删除该槽。
冻结与 XID 环绕防护
PostgreSQL 的 XID 是 32 位无符号整数,约 42 亿个。为了避免「环绕」导致新旧判断错乱,VACUUM 会把足够老的元组冻结(Freeze),即把它们的 xmin 改写为特殊的 FrozenTransactionId,从此不再参与 XID 比较。
触发冻结的相关参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
autovacuum_freeze_max_age | 200000000(2 亿) | 表年龄达到该值时,强制触发 autovacuum(即使 autovacuum 被关闭,该防护仍然生效) |
vacuum_freeze_min_age | 50000000(5 千万) | 低于该年龄的元组不冻结(避免过早冻结增加 WAL) |
vacuum_freeze_table_age | 150000000(1.5 亿) | 表年龄超过该值时,VACUUM 做全表扫描而非仅扫描 VM 标记页 |
autovacuum_freeze_max_age 上限 | 2000000000 | 可设的最大值(留出安全余量) |
-- 查看所有数据库的 XID 年龄(用于评估环绕风险)
SELECT
datname,
age(datfrozenxid) AS xid_age,
current_setting('autovacuum_freeze_max_age')::bigint AS freeze_max_age,
round(100.0 * age(datfrozenxid)
/ current_setting('autovacuum_freeze_max_age')::bigint, 2) AS pct_of_limit,
datfrozenxid
FROM pg_database
ORDER BY xid_age DESC; datname | xid_age | freeze_max_age | pct_of_limit | datfrozenxid
-----------+-----------+----------------+--------------+--------------
postgres | 18421903 | 200000000 | 9.21 | 18421903
app_db | 98421103 | 200000000 | 49.21 | 98421103
log_db | 182004112 | 200000000 | 91.00 | 182004112-- 查看表的 XID 年龄:找出最老的表
SELECT
n.nspname || '.' || c.relname AS table_name,
age(c.relfrozenxid) AS xid_age,
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 age(c.relfrozenxid) DESC
LIMIT 10; table_name | xid_age | total_size
---------------------+-----------+------------
public.t_order | 124091882 | 2841 MB
public.t_log_event | 118203441 | 9214 MB
public.t_user | 98233110 | 421 MB必须监控 pct_of_limit
当 pct_of_limit 接近 100% 时,PostgreSQL 会:
- 忽略
autovacuum = off设置,强制启动 autovacuum 去做冻结。 - 如果连这都失败(如
max_workers全被长事务占满),日志会出现:
WARNING: database "app_db" must be vacuumed within 1000000 transactions
HINT: To avoid a database shutdown, execute a database-wide VACUUM in that database.- 若最终用尽余量,PostgreSQL 会拒绝新的写事务(进入保护性只读状态),直到完成冻结。
因此 pct_of_limit 是最重要的告警项之一。经验阈值:
< 50%:健康。50% ~ 80%:关注,检查 autovacuum 是否被阻塞。> 80%:必须立即介入排查(长事务、复制槽、worker 不足)。
如何控制 autovacuum 的资源消耗
「autovacuum 影响业务」通常有两类诉求,处理方式完全不同:
诉求 A:autovacuum 太慢,清理跟不上,表在膨胀
# postgresql.conf —— 提高清理能力
autovacuum_max_workers = 6 # 从 3 提到 6(注意:会同步分薄 cost_limit)
autovacuum_vacuum_cost_limit = 1200 # 从 200 提到 1200(约 200 × workers)
autovacuum_vacuum_cost_delay = 2ms # 保持 2ms;SSD 上可考虑 0(不节流)
autovacuum_naptime = 30s # 从 1min 缩短到 30s,更快发现问题# postgresql.conf —— SSD / NVMe 上的激进配置
autovacuum_vacuum_cost_delay = 0ms # 完全不节流,让 autovacuum 全速跑
autovacuum_vacuum_cost_limit = 10000 # 成本上限提高(配合 delay=0,实际不再受限)autovacuum_vacuum_cost_delay = 0 的风险
设为 0 等于取消节流,autovacuum 会用尽可用 I/O 带宽。在共享存储(云盘、NAS、SAN)上,这可能会挤压其他实例或业务查询。
建议的中间路线:先设 autovacuum_vacuum_cost_delay = 1ms 并观察存储的 I/O 饱和度,再决定是否降到 0。
诉求 B:autovacuum 太猛,影响业务响应时间
# postgresql.conf —— 降低冲击(但会延长清理周期)
autovacuum_vacuum_cost_delay = 10ms # 从 2ms 提到 10ms,单 worker 更温柔
autovacuum_vacuum_cost_limit = 200 # 保持或降低
autovacuum_max_workers = 2 # 减少并行度-- 更精细的做法:只对「高峰时段的业务大表」单独放宽节流,其余表保持默认
ALTER TABLE t_order SET (
autovacuum_vacuum_cost_delay = 20, -- 该表清理时更温柔
autovacuum_vacuum_cost_limit = 100
);
-- 对「膨胀敏感、但不高频访问」的日志表,则加大力度
ALTER TABLE t_log_event SET (
autovacuum_vacuum_cost_delay = 0,
autovacuum_vacuum_scale_factor = 0.01
);可用的表级参数总览
-- 查看某张表已覆盖的参数(reloptions)
SELECT c.relname, c.reloptions
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relname = 't_order'; relname | reloptions
---------+-----------------------------------------------------------------
t_order | {autovacuum_vacuum_scale_factor=0.01,autovacuum_analyze_scale_factor=0.005}| 表级参数 | 对应全局参数 | 用途 |
|---|---|---|
autovacuum_enabled | autovacuum | 单表启用/禁用(谨慎使用,不建议禁用) |
autovacuum_vacuum_threshold | autovacuum_vacuum_threshold | 单表固定阈值 |
autovacuum_vacuum_scale_factor | autovacuum_vacuum_scale_factor | 单表百分比阈值(最常用) |
autovacuum_analyze_threshold | autovacuum_analyze_threshold | 单表 ANALYZE 固定阈值 |
autovacuum_analyze_scale_factor | autovacuum_analyze_scale_factor | 单表 ANALYZE 百分比阈值(最常用) |
autovacuum_vacuum_cost_delay | autovacuum_vacuum_cost_delay | 单表节流睡眠 |
autovacuum_vacuum_cost_limit | autovacuum_vacuum_cost_limit | 单表成本上限 |
autovacuum_freeze_min_age | vacuum_freeze_min_age | 单表冻结最小年龄 |
autovacuum_freeze_max_age | autovacuum_freeze_max_age | 单表强制冻结年龄 |
autovacuum_vacuum_insert_threshold | 同名(PG 13+) | 纯 INSERT 触发阈值 |
autovacuum_vacuum_insert_scale_factor | 同名(PG 13+) | 纯 INSERT 触发百分比 |
-- 查询某表「下次触发 autovacuum 的预测阈值」
SELECT
relname,
n_live_tup,
n_dead_tup,
(SELECT setting::numeric FROM pg_settings WHERE name = 'autovacuum_vacuum_threshold')
+ (SELECT setting::numeric FROM pg_settings WHERE name = 'autovacuum_vacuum_scale_factor')
* n_live_tup AS vacuum_trigger_at,
n_dead_tup
- ((SELECT setting::numeric FROM pg_settings WHERE name = 'autovacuum_vacuum_threshold')
+ (SELECT setting::numeric FROM pg_settings WHERE name = 'autovacuum_vacuum_scale_factor')
* n_live_tup) AS dead_tup_margin
FROM pg_stat_user_tables
WHERE relname = 't_order'; relname | n_live_tup | n_dead_tup | vacuum_trigger_at | dead_tup_margin
---------+------------+------------+-------------------+-----------------
t_order | 4821033 | 1842122 | 964256.6 | 877865.4dead_tup_margin > 0 说明「已经超过阈值但仍未清理」——此时应检查 worker 是否不足、是否被长事务阻塞。
手动触发与「不要手动 VACUUM」的边界
-- 手动对单表做 VACUUM ANALYZE(会更新 FSM/VM 与统计信息)
VACUUM (ANALYZE, VERBOSE) t_order;-- 只做冻结推进(大幅降低后续 autovacuum 压力)
VACUUM (FREEZE, ANALYZE) t_order;-- 查看上一条 VACUUM 的详细输出(VERBOSE)
VACUUM (VERBOSE) t_order;INFO: vacuuming "public.t_order"
INFO: scanned index "t_order_pkey" to remove 1842104 row versions
DETAIL: CPU: user: 2.41 s, system: 0.88 s, elapsed: 9.12 s
INFO: "t_order": removed 1842104 row versions in 142011 pages
DETAIL: CPU: user: 2.98 s, system: 1.02 s, elapsed: 11.24 s
INFO: index "t_order_pkey" now contains 4821033 row versions in 32044 pages
DETAIL: 1842104 index row versions were removed.
...
INFO: "t_order": found 1842104 removable, 4821033 nonremovable row versions in 620911 out of 620911 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 18491102oldest xmin 是关键的诊断信息
输出里的 oldest xmin: 18491102 表示「本次 VACUUM 认为仍然可见的最老事务」。如果它远小于当前 XID,说明有老快照存在,导致很多死元组无法被回收。
如果出现 N dead row versions cannot be removed yet,就应当立刻去查长事务与复制槽。
-- 整库 VACUUM(不阻塞读写,但会长时间占用 I/O)
-- 注意:需在目标库中执行,且 VACUUM FULL 才是重写表
VACUUM (ANALYZE);-- 整实例冻结(multixact / xid 双推进),用于紧急场景
VACUUM FREEZE; -- 单库# 命令行整实例冻结(连接每个库逐一执行,PG 官方推荐脚本 vacuumdb)
vacuumdb --all --freeze --analyze --verbose --jobs=4vacuumdb --all --freeze 的并发风险
--jobs=4 会并行处理多个库,但每个库内仍然串行。若实例上有大表,该操作可能持续数小时并占用大量 I/O。在紧急冻结场景下,可先处理 age(datfrozenxid) 最大的库。
实战示例
示例 1:完整诊断「autovacuum 不工作」
-- 步骤 1:确认 autovacuum 是否开启
SELECT name, setting, source, pending_restart
FROM pg_settings
WHERE name IN ('autovacuum', 'autovacuum_max_workers', 'autovacuum_naptime',
'autovacuum_vacuum_cost_limit', 'autovacuum_vacuum_cost_delay'); name | setting | source | pending_restart
-----------------------------+---------+---------+-----------------
autovacuum | on | default | f
autovacuum_max_workers | 3 | default | f
autovacuum_naptime | 1min | default | f
autovacuum_vacuum_cost_delay| 2 | default | f
autovacuum_vacuum_cost_limit| -1 | default | f-- 步骤 2:确认是否有 worker 在运行
SELECT pid, backend_type, state, query_start, left(query, 80) AS query
FROM pg_stat_activity
WHERE backend_type IN ('autovacuum launcher', 'autovacuum worker'); pid | backend_type | state | query_start | query
-------+---------------------+--------+------------------------------+-------------------
12041 | autovacuum launcher | idle | |
20112 | autovacuum worker | active | 2026-09-22 09:18:02.113+08 | autovacuum: VACUUM public.t_order-- 步骤 3:找出「最该被清理但一直没清」的表
SELECT
schemaname,
relname,
n_dead_tup,
n_live_tup,
last_autovacuum,
CASE WHEN last_autovacuum IS NULL THEN '从未自动清理'
ELSE (now() - last_autovacuum)::text
END AS since_last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > coalesce(
(SELECT setting::numeric FROM pg_settings WHERE name = 'autovacuum_vacuum_threshold')
+ (SELECT setting::numeric FROM pg_settings WHERE name = 'autovacuum_vacuum_scale_factor')
* n_live_tup, 0)
ORDER BY n_dead_tup DESC
LIMIT 10; schemaname | relname | n_dead_tup | n_live_tup | last_autovacuum | since_last_autovacuum
------------+-------------------+------------+------------+------------------------+----------------------
public | t_user_session | 882331 | 212044 | 2026-09-20 03:11:02+08 | 2 days 06:12:41.113
public | t_order | 1842122 | 4821033 | 2026-09-22 09:02:41+08 | 00:18:22.114-- 步骤 4:检查是否有长事务 / 老快照阻塞
SELECT
pid,
state,
backend_xmin,
now() - xact_start AS xact_age,
left(query, 60) AS query
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY backend_xmin::text::bigint
LIMIT 5; pid | state | backend_xmin | xact_age | query
-------+---------------------+--------------+--------------+--------------------------
18422 | idle in transaction | 18421903 | 02:14:38.221 | SELECT * FROM t_user-- 步骤 5:检查是否有不活跃的复制槽
SELECT slot_name, slot_type, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots
WHERE active = false;诊断结论:t_user_session 已 2 天未被清理,且存在一个挂了 2 小时的 idle in transaction 会话。这个老快照钉住了 XID,使 autovacuum 无法回收死元组。处理方式是先终止长事务,而不是调 autovacuum 参数。
示例 2:用表级参数治「大表不清理」
-- 结论:t_order 有 482 万行,默认阈值 = 50 + 0.2 × 4821033 ≈ 96 万死元组
-- 而当前已有 184 万死元组,但只要超过阈值就该被清理 —— 说明是「清理了但追不上」
-- 对策:降低比例阈值,让每次清理的量更小、更频繁
ALTER TABLE t_order SET (
autovacuum_vacuum_scale_factor = 0.01, -- 20% → 1%(阈值降到约 4.8 万)
autovacuum_analyze_scale_factor = 0.005, -- 10% → 0.5%
autovacuum_vacuum_threshold = 500,
autovacuum_analyze_threshold = 500
);-- 验证参数已生效
SELECT relname, reloptions
FROM pg_class
WHERE relname = 't_order'; relname | reloptions
---------+-----------------------------------------------------------------------------------------------------
t_order | {autovacuum_vacuum_scale_factor=0.01,autovacuum_analyze_scale_factor=0.005,autovacuum_vacuum_threshold=500,autovacuum_analyze_threshold=500}-- 观察后续一天内 t_order 的自动清理次数(last_autovacuum 会频繁刷新)
SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 't_order'; relname | n_dead_tup | last_autovacuum
---------+------------+------------------------
t_order | 41203 | 2026-09-22 11:41:02+08n_dead_tup 从 184 万降到 4.1 万——说明清理已经跟得上产生速度了。
示例 3:评估索引数量对 autovacuum 成本的影响
VACUUM 的成本与索引数量近似成正比,因为每个索引都要被扫描和清理。
-- 统计每张表的索引数量与 autovacuum 历史
SELECT
t.schemaname,
t.relname,
count(i.indexrelid) AS index_count,
t.n_dead_tup,
t.last_autovacuum,
pg_size_pretty(pg_total_relation_size(t.relid)) AS total_size
FROM pg_stat_user_tables t
LEFT JOIN pg_stat_user_indexes i ON i.relid = t.relid
GROUP BY t.schemaname, t.relname, t.relid, t.n_dead_tup, t.last_autovacuum
ORDER BY index_count DESC
LIMIT 10; schemaname | relname | index_count | n_dead_tup | total_size
------------+------------------+-------------+------------+------------
public | t_order | 9 | 41203 | 2841 MB
public | t_order_item | 6 | 182033 | 6122 MB
public | t_user | 4 | 8213 | 421 MB冗余索引是 autovacuum 的隐形负担
9 个索引意味着每次 VACUUM 要额外扫描并清理 9 棵 B-tree。实践中常见「同列上的重复索引」或「从没被使用过的索引」。
-- 找出从未被使用过的索引(PG 16+ 字段名 idx_scan)
SELECT
schemaname,
relname,
indexrelname,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 10;删除无用索引既能提升写入性能,也能显著降低每次 autovacuum 的耗时。
示例 4:用日志监控 autovacuum 行为
# postgresql.conf —— 记录每个 autovacuum 的动作与耗时
log_autovacuum_min_duration = 0 # 0 表示记录全部;也可设 250ms 只记录慢的
log_min_duration_statement = -1 # 保持关闭,避免日志被 SQL 淹没# 日志输出示例
2026-09-22 09:18:02.113 CST [20112] LOG: automatic vacuum of table "app_db.public.t_order": index scans: 9
pages: 0 removed, 620911 remain, 142011 scanned (22.87% of total)
tuples: 1842104 removed, 4821033 remain, 0 are dead but not yet removable
removable cutoff: 18491102, which was 41203 XIDs old when operation ended
new relfrozenxid: 18491102, which is 1 XIDs ahead of previous value
index scan needed: 142011 pages from table (22.87% of total) had 1842104 dead item identifiers removed
index "t_order_pkey": pages: 32044 in total, 0 newly deleted, 0 currently deleted, 0 reusable
I/O timings: read: 1204.183 ms, write: 0.000 ms
avg read rate: 42.113 MB/s, avg write rate: 0.000 MB/s
buffer usage: 168421 hits, 142011 misses, 8421 dirtied
WAL usage: 8421 records, 0 full page images, 1204 bytes
system usage: CPU: user: 2.41 s, system: 0.88 s, elapsed: 9.12 s关键字段解读:
pages: 0 removed, 620911 remain:VACUUM 通常不缩短文件(removed 为 0 是正常的,空间被标记为可复用)。0 are dead but not yet removable:没有死元组被老快照卡住,健康。avg read rate: 42.113 MB/s:这是autovacuum_vacuum_cost_delay节流后的实际速率。new relfrozenxid:冻结推进情况。removable cutoff: ... which was 41203 XIDs old:这个值越大,说明清理越落后(理想情况是接近 0)。
log_autovacuum_min_duration = 0 在高频小表场景下会刷爆日志
如果你有很多高频更新的小表,autovacuum 会非常频繁地触发,0 会导致日志量爆炸。
推荐:先设 0 观察一两天搞清楚基线,然后改成 250ms 或 1s,只保留值得关注的慢清理。
示例 5:紧急 XID 冻结处理流程
-- 1) 找出年龄最大的库
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC; datname | xid_age
-----------+-----------
log_db | 182004112
app_db | 98421103-- 2) 找出该库中年龄最大的表
SELECT
n.nspname || '.' || c.relname AS table_name,
age(c.relfrozenxid) AS xid_age,
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 age(c.relfrozenxid) DESC
LIMIT 5; table_name | xid_age | total_size
-------------------+-----------+------------
public.t_log_old | 181920441 | 42 GB
public.t_log_event| 178203441 | 9214 MB-- 3) 先清理长事务,释放老快照(否则冻结无法推进)
SELECT pid, state, now() - xact_start AS age, left(query, 60)
FROM pg_stat_activity
WHERE xact_start < now() - interval '10 minutes'
ORDER BY xact_start;-- 4) 对大表手动 VACUUM FREEZE(分表处理,避免一次占用过多 I/O)
VACUUM (FREEZE, ANALYZE, VERBOSE) t_log_old;INFO: aggressively vacuuming "public.t_log_old"
INFO: "t_log_old": found 1842 removable, 128410021 nonremovable row versions in 5482110 out of 5482110 pages
...
INFO: new relfrozenxid: 182004112-- 5) 验证年龄已回落
SELECT datname, age(datfrozenxid) AS xid_age FROM pg_database WHERE datname = 'log_db'; datname | xid_age
---------+---------
log_db | 1842119从 1.82 亿降到 184 万 —— 冻结推进成功。
VACUUM (FREEZE) 会写大量 WAL
全表冻结会重写每个页的元组头,产生可观的 WAL 量(可能达到表大小的数倍)。在复制环境与 WAL 归档场景下要评估 WAL 空间与网络带宽。
PG 17+ 对冻结 WAL 的生成有优化,但仍然是「大操作」。建议在业务低峰执行,并在执行前确认:
- WAL 磁盘剩余空间充足。
- 归档/复制链路未积压。
- 有回滚计划(虽然 VACUUM 本身可安全中断,但已推进的部分不会回退,这恰好是我们想要的)。
注意事项
- 不要关闭 autovacuum:即使关闭,达到
autovacuum_freeze_max_age时仍会被强制触发;关闭只会让膨胀失控。 autovacuum_max_workers需要重启:它是postmaster级参数。autovacuum_vacuum_cost_limit是全体 worker 共享的总额:增加 worker 数会分薄每个 worker 的可用成本。- 长事务与不活跃复制槽是 autovacuum 的头号敌人:因为它们让死元组「不可回收」,调任何参数都没用。
- VACUUM 通常不回收磁盘空间给操作系统:只标记为表内可复用。要真正缩短文件需
VACUUM FULL(会长时间持有ACCESS EXCLUSIVE锁)或pg_repack。 VACUUM FULL与CLUSTER是重量级操作:它们重写整表,需要与表等大的额外磁盘空间,且阻塞一切读写。- autovacuum worker 不计入
max_connections:它们从autovacuum_max_workers与max_worker_processes的共享池中分配。 n_dead_tup是估算值:来自统计收集器,可能有延迟或偏差(PG 15+ 改为共享内存统计,实时性更好但仍是近似)。last_autovacuum为 NULL 只表示「自统计重置以来没有」:pg_stat_reset()后该字段会清空,不代表从未清理过。autovacuum_vacuum_insert_threshold需要 PG 13+:在更早版本,纯 INSERT 不会触发 VACUUM(只会触发 ANALYZE),导致「只增不改」的表也堆积死元组的场景无解;PG 13+ 引入该参数解决此问题(如 append-only 表因INSERT产生的LP_UNUSED与 VM 更新需求)。
-- 不推荐:靠禁用 autovacuum 来「消除性能抖动」
-- ALTER SYSTEM SET autovacuum = off; -- 不推荐:会导致膨胀失控与 XID 逼近上限-- 不推荐:对业务大表执行 VACUUM FULL 来「回收空间」
-- VACUUM FULL t_order; -- 不推荐:长时间 ACCESS EXCLUSIVE 锁,业务全停-- 不推荐:用极其宽松的节流参数让 autovacuum 全速跑而不做监控
-- ALTER SYSTEM SET autovacuum_vacuum_cost_delay = 0; -- 不推荐:可能打满共享存储 I/O最佳实践
- 给大表加表级参数:
autovacuum_vacuum_scale_factor设到0.01~0.02,让阈值落在可控量级。 cost_limit与max_workers同比例提升:经验公式autovacuum_vacuum_cost_limit ≈ 200 × autovacuum_max_workers。- SSD 上可把
autovacuum_vacuum_cost_delay降到 0~1ms:机械盘保持 2ms 或更大。 - 把
age(datfrozenxid)纳入核心告警:超过autovacuum_freeze_max_age的 50% 就要关注,超过 80% 立即介入。 - 定期巡检长事务与不活跃复制槽:把「最老
backend_xmin」和「不活跃槽的 WAL 保留量」做成监控面板。 - 定期清理无用索引:索引越多,VACUUM 越慢;用
pg_stat_user_indexes.idx_scan = 0找候选。 - 开启
log_autovacuum_min_duration(250ms~1s):这是排查膨胀问题最直接的证据来源。 - 优先用
pg_repack替代VACUUM FULL:pg_repack只需短时锁表,适合在线收缩。 - 不要给高频小表设过激进的参数:会导致 autovacuum 几乎不停歇地扫描小表,得不偿失(应针对性地选阈值)。
- 理解「膨胀是常态,可控即可」:完全不膨胀不现实;目标是让膨胀率稳定在一个可接受区间(如 < 20%)。
下一节
继续学习:Checkpointer(检查点进程)