Skip to content

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 WraparoundXID 用尽(32 位)导致的灾难性问题,靠冻结(Freeze)避免
冻结Freeze把元组的 xmin 标记为「永久可见」,使其不再受 XID 比较约束
冻结映射FrozenMap(pg_visibility)可见性映射(VM)中描述「该页所有元组均已冻结」的位
成本延迟Cost-based Vacuum Delay累计达到 autovacuum_vacuum_cost_limit 后主动睡眠,控制 I/O 冲击
统计信息收集器Statistics CollectorPG 15 前为独立进程;PG 15+ 改为共享内存统计
表级参数Storage Parameters可在表上单独覆盖 autovacuum 全局阈值(ALTER TABLE ... SET)

Autovacuum 不是「可选优化」,而是必需机制

很多团队习惯性关闭 autovacuum(认为它「乱抢 I/O」),这是极其危险的做法。它同时承担两项不可替代的职责:

  1. 回收死元组空间(VACUUM)——否则表和索引会无限膨胀。
  2. 推进冻结、防止 XID 环绕(VACUUM FREEZE)——否则数据库会在达到 autovacuum_freeze_max_age 后被迫停机进行冻结,或者在极端情况下数据损坏。

正确做法是调优它(降低单表冲击、提高覆盖度),而不是关闭它。若确实需要避开业务高峰,应该用 autovacuum 的表级参数与调度策略,而不是全局关闭。

进程结构 ​

Autovacuum 是「1 个 launcher + N 个 worker」的两级结构,这与「常驻单进程」的 bgwriter / walwriter 不同:

text
                    ┌──────────────────────────────┐
                    │   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)
sql
-- 查看各类后台进程(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;
text
    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)醒来一次,做以下事情:

text
① 读取本实例的数据库列表
② 对每个数据库,判断「是否需要清理」:
     - 该库最老的表,死元组数是否超过 autovacuum_vacuum_threshold
     - 该库最老的表,是否接近 autovacuum_freeze_max_age
③ 把需要处理的库放入「需求列表」
④ 若有空闲 worker 槽位,从列表取一个库,派生一个 worker 去处理
⑤ 继续 naptime 睡眠

关键点:launcher 是按「数据库」粒度分派的,worker 是按「表」粒度工作的。一个 worker 连到某个库后,会遍历该库中所有「需要清理」的表,逐张处理。

text
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 的触发阈值公式:

text
触发自动 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_threshold50固定基数(元组数)
autovacuum_vacuum_scale_factor0.2表大小的百分比
autovacuum_analyze_threshold50固定基数
autovacuum_analyze_scale_factor0.1表大小的百分比
autovacuum_vacuum_insert_threshold1000(PG 13+)纯 INSERT 触发的基数
autovacuum_vacuum_insert_scale_factor0.2(PG 13+)纯 INSERT 触发的百分比
text
假设表有 1,000,000 行(均为活元组),默认参数下:

  VACUUM 阈值  = 50 + 0.2  × 1,000,000 = 200,050 个死元组
  ANALYZE 阈值 = 50 + 0.1  × 1,000,000 = 100,050 个被修改元组

这解释了一个经典问题:大表 20% 的阈值太高。

text
表有 100,000,000 行时:
  VACUUM 阈值 = 50 + 0.2 × 100,000,000 = 20,000,050 个死元组

也就是说,要积累两千万个死元组才会触发一次 VACUUM。
在这期间表可能已经膨胀到原来的数倍,而且单次 VACUUM 要处理两千万个死元组,
耗时极长、冲击极大 —— 形成「越不清理越难清理」的恶性循环。

大表必须用表级参数覆盖

对大表(百万行以上),必须用 ALTER TABLE ... SET (...) 覆盖全局阈值:

sql
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 设了很严格的节流——所以它其实已经很「温柔」了,问题往往出在「太温柔导致清理不完」。

节流机制基于一套成本模型:

操作成本(默认)参数
从共享缓冲区命中读一页1vacuum_cost_page_hit
从磁盘读一页(未命中缓冲区)2vacuum_cost_page_miss(PG 17+ 默认由 10 降为 2)
写一个脏页20vacuum_cost_page_dirty
text
节流逻辑(每个 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 上这远远用不满设备能力。

sql
-- 查看当前节流参数
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;
text
            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       |      | user

autovacuum_vacuum_cost_limit = -1 的含义

-1 表示「继承 vacuum_cost_limit 的值」(默认 200)。这是 PG 里少见的「继承式默认值」。

另外要注意一个历史遗留行为:autovacuum_vacuum_cost_limit 是所有 autovacuum worker 共享的总限额(而不是每个 worker 独立)——它会被平均分配给当前活跃的 worker。所以:

text
若 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 会执行两类任务,它们的目标完全不同:

维度VACUUMANALYZE
目标回收死元组空间、推进冻结、更新可见性映射采样表内容、生成优化器所需的统计信息
是否改数据文件是(标记空间可复用,通常不缩短文件)否(只读采样)
是否拿排他锁否(SHARE UPDATE EXCLUSIVE,不阻塞读写)否(SHARE UPDATE EXCLUSIVE)
主要产物FSM 更新、VM 更新、relfrozenxid 推进pg_statistic / pg_stats 更新
相关统计视图pg_stat_all_tables.n_dead_tup、last_autovacuumpg_stat_all_tables.last_autoanalyze
sql
-- 查看哪些表最需要被处理:按死元组占比排序
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;
text
 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 之后产生的所有死元组都不能被回收。

text
会话 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。

sql
-- 找出当前所有后端持有的最老事务快照(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;
text
  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
sql
-- 查看某个会话是否阻塞了 autovacuum 回收
SELECT pg_blocking_pids(18422);
sql
-- 计算「最老快照」导致的元组积压量(需 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。这类问题往往在几周后才通过磁盘爆满被发现。

必须监控:

sql
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_age200000000(2 亿)表年龄达到该值时,强制触发 autovacuum(即使 autovacuum 被关闭,该防护仍然生效)
vacuum_freeze_min_age50000000(5 千万)低于该年龄的元组不冻结(避免过早冻结增加 WAL)
vacuum_freeze_table_age150000000(1.5 亿)表年龄超过该值时,VACUUM 做全表扫描而非仅扫描 VM 标记页
autovacuum_freeze_max_age 上限2000000000可设的最大值(留出安全余量)
sql
-- 查看所有数据库的 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;
text
  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
sql
-- 查看表的 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;
text
     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 会:

  1. 忽略 autovacuum = off 设置,强制启动 autovacuum 去做冻结。
  2. 如果连这都失败(如 max_workers 全被长事务占满),日志会出现:
text
WARNING:  database "app_db" must be vacuumed within 1000000 transactions
HINT:  To avoid a database shutdown, execute a database-wide VACUUM in that database.
  1. 若最终用尽余量,PostgreSQL 会拒绝新的写事务(进入保护性只读状态),直到完成冻结。

因此 pct_of_limit 是最重要的告警项之一。经验阈值:

  • < 50%:健康。
  • 50% ~ 80%:关注,检查 autovacuum 是否被阻塞。
  • > 80%:必须立即介入排查(长事务、复制槽、worker 不足)。

如何控制 autovacuum 的资源消耗 ​

「autovacuum 影响业务」通常有两类诉求,处理方式完全不同:

诉求 A:autovacuum 太慢,清理跟不上,表在膨胀

ini
# 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,更快发现问题
ini
# 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 太猛,影响业务响应时间

ini
# postgresql.conf —— 降低冲击(但会延长清理周期)
autovacuum_vacuum_cost_delay = 10ms     # 从 2ms 提到 10ms,单 worker 更温柔
autovacuum_vacuum_cost_limit = 200      # 保持或降低
autovacuum_max_workers = 2              # 减少并行度
sql
-- 更精细的做法:只对「高峰时段的业务大表」单独放宽节流,其余表保持默认
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
);

可用的表级参数总览 ​

sql
-- 查看某张表已覆盖的参数(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';
text
 relname |                           reloptions
---------+-----------------------------------------------------------------
 t_order | {autovacuum_vacuum_scale_factor=0.01,autovacuum_analyze_scale_factor=0.005}
表级参数对应全局参数用途
autovacuum_enabledautovacuum单表启用/禁用(谨慎使用,不建议禁用)
autovacuum_vacuum_thresholdautovacuum_vacuum_threshold单表固定阈值
autovacuum_vacuum_scale_factorautovacuum_vacuum_scale_factor单表百分比阈值(最常用)
autovacuum_analyze_thresholdautovacuum_analyze_threshold单表 ANALYZE 固定阈值
autovacuum_analyze_scale_factorautovacuum_analyze_scale_factor单表 ANALYZE 百分比阈值(最常用)
autovacuum_vacuum_cost_delayautovacuum_vacuum_cost_delay单表节流睡眠
autovacuum_vacuum_cost_limitautovacuum_vacuum_cost_limit单表成本上限
autovacuum_freeze_min_agevacuum_freeze_min_age单表冻结最小年龄
autovacuum_freeze_max_ageautovacuum_freeze_max_age单表强制冻结年龄
autovacuum_vacuum_insert_threshold同名(PG 13+)纯 INSERT 触发阈值
autovacuum_vacuum_insert_scale_factor同名(PG 13+)纯 INSERT 触发百分比
sql
-- 查询某表「下次触发 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';
text
 relname | n_live_tup | n_dead_tup | vacuum_trigger_at | dead_tup_margin
---------+------------+------------+-------------------+-----------------
 t_order |    4821033 |    1842122 |          964256.6 |        877865.4

dead_tup_margin > 0 说明「已经超过阈值但仍未清理」——此时应检查 worker 是否不足、是否被长事务阻塞。

手动触发与「不要手动 VACUUM」的边界 ​

sql
-- 手动对单表做 VACUUM ANALYZE(会更新 FSM/VM 与统计信息)
VACUUM (ANALYZE, VERBOSE) t_order;
sql
-- 只做冻结推进(大幅降低后续 autovacuum 压力)
VACUUM (FREEZE, ANALYZE) t_order;
sql
-- 查看上一条 VACUUM 的详细输出(VERBOSE)
VACUUM (VERBOSE) t_order;
text
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: 18491102

oldest xmin 是关键的诊断信息

输出里的 oldest xmin: 18491102 表示「本次 VACUUM 认为仍然可见的最老事务」。如果它远小于当前 XID,说明有老快照存在,导致很多死元组无法被回收。

如果出现 N dead row versions cannot be removed yet,就应当立刻去查长事务与复制槽。

sql
-- 整库 VACUUM(不阻塞读写,但会长时间占用 I/O)
-- 注意:需在目标库中执行,且 VACUUM FULL 才是重写表
VACUUM (ANALYZE);
sql
-- 整实例冻结(multixact / xid 双推进),用于紧急场景
VACUUM FREEZE;   -- 单库
bash
# 命令行整实例冻结(连接每个库逐一执行,PG 官方推荐脚本 vacuumdb)
vacuumdb --all --freeze --analyze --verbose --jobs=4

vacuumdb --all --freeze 的并发风险

--jobs=4 会并行处理多个库,但每个库内仍然串行。若实例上有大表,该操作可能持续数小时并占用大量 I/O。在紧急冻结场景下,可先处理 age(datfrozenxid) 最大的库。

实战示例 ​

示例 1:完整诊断「autovacuum 不工作」 ​

sql
-- 步骤 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');
text
            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
sql
-- 步骤 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');
text
  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
sql
-- 步骤 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;
text
 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
sql
-- 步骤 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;
text
  pid  |        state        | backend_xmin |   xact_age   |          query
-------+---------------------+--------------+--------------+--------------------------
 18422 | idle in transaction |     18421903 | 02:14:38.221 | SELECT * FROM t_user
sql
-- 步骤 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:用表级参数治「大表不清理」 ​

sql
-- 结论: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
);
sql
-- 验证参数已生效
SELECT relname, reloptions
FROM pg_class
WHERE relname = 't_order';
text
 relname |                                            reloptions
---------+-----------------------------------------------------------------------------------------------------
 t_order | {autovacuum_vacuum_scale_factor=0.01,autovacuum_analyze_scale_factor=0.005,autovacuum_vacuum_threshold=500,autovacuum_analyze_threshold=500}
sql
-- 观察后续一天内 t_order 的自动清理次数(last_autovacuum 会频繁刷新)
SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 't_order';
text
 relname | n_dead_tup |    last_autovacuum
---------+------------+------------------------
 t_order |      41203 | 2026-09-22 11:41:02+08

n_dead_tup 从 184 万降到 4.1 万——说明清理已经跟得上产生速度了。

示例 3:评估索引数量对 autovacuum 成本的影响 ​

VACUUM 的成本与索引数量近似成正比,因为每个索引都要被扫描和清理。

sql
-- 统计每张表的索引数量与 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;
text
 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。实践中常见「同列上的重复索引」或「从没被使用过的索引」。

sql
-- 找出从未被使用过的索引(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 行为 ​

ini
# postgresql.conf —— 记录每个 autovacuum 的动作与耗时
log_autovacuum_min_duration = 0        # 0 表示记录全部;也可设 250ms 只记录慢的
log_min_duration_statement = -1        # 保持关闭,避免日志被 SQL 淹没
text
# 日志输出示例
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 冻结处理流程 ​

sql
-- 1) 找出年龄最大的库
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;
text
  datname  |  xid_age
-----------+-----------
 log_db    | 182004112
 app_db    |  98421103
sql
-- 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;
text
    table_name     |  xid_age  | total_size
-------------------+-----------+------------
 public.t_log_old  | 181920441 | 42 GB
 public.t_log_event| 178203441 | 9214 MB
sql
-- 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;
sql
-- 4) 对大表手动 VACUUM FREEZE(分表处理,避免一次占用过多 I/O)
VACUUM (FREEZE, ANALYZE, VERBOSE) t_log_old;
text
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
sql
-- 5) 验证年龄已回落
SELECT datname, age(datfrozenxid) AS xid_age FROM pg_database WHERE datname = 'log_db';
text
 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 更新需求)。
sql
-- 不推荐:靠禁用 autovacuum 来「消除性能抖动」
-- ALTER SYSTEM SET autovacuum = off;   -- 不推荐:会导致膨胀失控与 XID 逼近上限
sql
-- 不推荐:对业务大表执行 VACUUM FULL 来「回收空间」
-- VACUUM FULL t_order;                 -- 不推荐:长时间 ACCESS EXCLUSIVE 锁,业务全停
sql
-- 不推荐:用极其宽松的节流参数让 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(检查点进程)

参考链接 ​