# autovacuum 的触发与资源

LLMS 索引： [llms.txt](/llms.txt)

---

autovacuum 不是“每隔一分钟把所有表 vacuum 一遍”。

它是一套按数据库调度、按表判断资格、按 worker 执行、按 cost budget 限速的后台系统：

```text
launcher
  -> choose database
      -> worker examines relations
          -> trigger decision
              -> VACUUM / ANALYZE / both
                  -> resource and lock interaction
```

排障必须分开问：

1. 表有没有达到触发条件？
2. 有 worker 能接活吗？
3. worker 启动后在做什么？
4. 为什么扫完仍留下旧版本？

“把 scale factor 调小”最多回答第一个问题的一部分。

## 28.2.1 阈值、比例、插入触发与表级覆盖 {#item-28-2-1}

### UPDATE/DELETE 触发公式

PostgreSQL 18 对普通 vacuum 的变化量先计算为：

$$
T_{\text{raw}} =
T_{\text{base}} + f_{\text{vacuum}} \times N_{\text{table}}
$$

对应：

```text
T_base   autovacuum_vacuum_threshold
f        autovacuum_vacuum_scale_factor
N_table  pg_class.reltuples
T_max    autovacuum_vacuum_max_threshold
```

当 $T_{\max}\ge 0$ 时，最终阈值为：

$$
T_{\text{vacuum}} = \min(T_{\max}, T_{\text{raw}})
$$

当 `autovacuum_vacuum_max_threshold=-1` 时，表示**禁用最大阈值**，最终阈值就是
$T_{\text{raw}}$，不能把 `-1` 直接代入 `min()`。

当自上次 vacuum 以来被 UPDATE/DELETE 变旧的 tuple 估计数超过这个阈值，表取得
vacuum 资格。

PostgreSQL 18 引入/使用 `autovacuum_vacuum_max_threshold` 作为上限：

```text
base=500
scale=0.08
max=100,000,000
reltuples=1,000,000,000

base + scale * rows = 80,000,500
effective threshold = 80,000,500
```

若表为 10 billion rows：

```text
base + scale * rows = 800,000,500
effective threshold = 100,000,000
```

版本低于 PostgreSQL 18 时，不要照抄这个公式中的 max 项；先查对应 major 文档和
`pg_settings` 是否存在。

### INSERT-only 也需要 vacuum

只插不删的表没有 dead tuple，却仍需要：

- 更新 visibility map；
- 让 index-only scan 受益；
- 冻结旧 XID；
- 降低以后 aggressive vacuum 的工作。

插入触发公式为：

$$
T_{\text{insert}} =
T_{\text{insert-base}} +
f_{\text{insert}} \times N_{\text{table}} \times
\left(1 - \frac{\text{relallfrozen}}{\text{relpages}}\right)
$$

对应：

```text
autovacuum_vacuum_insert_threshold
autovacuum_vacuum_insert_scale_factor
pg_class.reltuples
unfrozen page fraction
```

这不是简单的：

```text
1000 + 0.2 * rows
```

它还乘以“未冻结页面比例”。当表逐步 all-frozen，insert-based 触发的 scale 部分也会
变化。

### ANALYZE 有自己的阈值

$$
T_{\text{analyze}} =
T_{\text{analyze-base}} +
f_{\text{analyze}} \times N_{\text{table}}
$$

变化量包括 insert/update/delete。vacuum 和 analyze 可能：

```text
only vacuum
only analyze
vacuum then analyze
```

不要把 `last_autovacuum` 当 `last_autoanalyze`。

### freeze 资格优先于普通变化量

当 `relfrozenxid` 年龄超过 `autovacuum_freeze_max_age`，系统会强制 vacuum，即使：

- 普通 `autovacuum` GUC 为 off；
- 表级 `autovacuum_enabled=false`；
- dead tuple 没达到普通阈值。

同理，multixact 有独立的：

```text
relminmxid
autovacuum_multixact_freeze_max_age
```

因此“关 autovacuum”既不安全，也不能保证后台永远不出现 worker；防回卷维护是正确性
机制，不是可选性能功能。

### `reltuples` 和 change count 都不是精确实时值

触发器依赖：

- `pg_class.reltuples` 估计；
- cumulative statistics 的变化计数；
- 最近 vacuum/analyze 更新；
- stats flush 的最终一致性。

边界附近出现几秒或一轮调度差异是正常的。排障时先查看实际输入：

```sql
WITH p AS (
  SELECT
    current_setting('autovacuum_vacuum_threshold')::numeric AS base,
    current_setting('autovacuum_vacuum_scale_factor')::numeric AS scale,
    current_setting('autovacuum_vacuum_max_threshold')::numeric AS max_t
)
SELECT
  s.schemaname,
  s.relname,
  c.reltuples,
  s.n_dead_tup,
  CASE
    WHEN p.max_t < 0 THEN p.base + p.scale * c.reltuples
    ELSE least(p.max_t, p.base + p.scale * c.reltuples)
  END AS estimated_trigger,
  s.last_autovacuum
FROM pg_stat_user_tables AS s
JOIN pg_class AS c ON c.oid = s.relid
CROSS JOIN p
ORDER BY
  s.n_dead_tup
  / nullif(
      CASE
        WHEN p.max_t < 0 THEN p.base + p.scale * c.reltuples
        ELSE least(p.max_t, p.base + p.scale * c.reltuples)
      END,
      0
    )
  DESC NULLS LAST;
```

这段查询仍没处理表级覆盖，生产版需要把 `reloptions` 合并进来。

### 表级覆盖：治疗特殊表，不复制全局配置

高 churn 大表、append-only 表和小型 catalog-like 表，触发策略可能不同：

```sql
ALTER TABLE app.hot_orders SET (
  autovacuum_vacuum_threshold = 1000,
  autovacuum_vacuum_scale_factor = 0.01,
  autovacuum_analyze_scale_factor = 0.02
);
```

查看：

```sql
SELECT
  n.nspname,
  c.relname,
  c.reloptions
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.reloptions IS NOT NULL
ORDER BY 1, 2;
```

恢复继承全局值：

```sql
ALTER TABLE app.hot_orders RESET (
  autovacuum_vacuum_threshold,
  autovacuum_vacuum_scale_factor,
  autovacuum_analyze_scale_factor
);
```

表级覆盖适合：

```text
one relation demonstrably misses its maintenance window
its write pattern differs materially from cluster norm
change rate and resource budget are measured
override is in schema/IaC and reviewed
```

不适合：

```text
copy every global GUC to every table
set autovacuum_enabled=false as tuning
hide a long-transaction blocker
raise freeze age to silence alerts
```

第 28 章实验为了让手工 vacuum 不被后台抢跑，在**一次性夹具表**上临时设置
`autovacuum_enabled=false`；数据库清理后该设置随表消失。公开结果明确将其列为
`fixture_table_autovacuum_enabled=false`，不是生产建议。

### 计算后还要看时间

一个表达到阈值只表示“有资格”，不表示立刻开始。launcher 要轮询数据库，worker 要
可用，其他 relation 可能排在前面。

评估维护能力，应比较：

$$
\text{dead tuple arrival rate}
\quad \text{vs} \quad
\text{vacuum reclamation rate}
$$

若每小时产生 500 million obsolete tuples，而可用 worker 每小时只能处理 300 million，
调低触发阈值只会更早开始积压，不能解决服务率不足。

## 28.2.2 worker、cost delay、I/O 与业务竞争 {#item-28-2-2}

### launcher、worker slot 和 worker 上限

PostgreSQL 18 需要同时理解：

```text
autovacuum_worker_slots
autovacuum_max_workers
autovacuum_naptime
number of databases
```

`autovacuum_worker_slots` 在启动时为 worker 预留 backend slot；
`autovacuum_max_workers` 是可同时运行 worker 的上限。把后者设得高于前者没有效果。

launcher 尝试把工作分散到各数据库；有 $N$ 个数据库时，会试图约每
`autovacuum_naptime / N` 启动一个 worker。它不是每个数据库独立一套无限 worker。

查询：

```sql
SELECT name, setting, unit, context, source, pending_restart
FROM pg_settings
WHERE name IN (
  'autovacuum',
  'autovacuum_worker_slots',
  'autovacuum_max_workers',
  'autovacuum_naptime'
)
ORDER BY name;
```

### worker 数只是并发上限

增加 worker 可能：

- 减少多个数据库/表的排队；
- 让更多表并发扫描；
- 同时增加 I/O、CPU、buffer churn；
- 放大 `autovacuum_work_mem` 总预算；
- 与 foreground query、checkpoint、backup、replay 竞争。

若瓶颈是单块磁盘，三个 worker 已把设备打满，再加三个只会提高 queue depth 和业务
tail latency。

先看：

```text
eligible tables waiting
active autovacuum workers
worker phase
disk latency / queue
CPU busy / run queue
buffer and cache effect
business p95/p99
replica and archive lag
```

### memory 按 worker 放大

`autovacuum_work_mem` 控制每个 autovacuum worker 可用的 maintenance memory；设为
`-1` 时回退到 `maintenance_work_mem`。粗略预算：

$$
M_{\text{auto}} \le
W_{\text{active}} \times M_{\text{per-worker}}
$$

这仍是上界近似，不是每个 worker 永远一次性占满。它用于预留最坏并发，而不是预测
RSS 精确值。

内存主要影响：

- 收集 dead item identifiers；
- index vacuum cycle 频率；
- maintenance 内部结构。

它不会让一个被旧 snapshot 保留的 tuple 突然可删。

PostgreSQL 18 的 `pg_stat_progress_vacuum` 暴露：

```text
max_dead_tuple_bytes
dead_tuple_bytes
num_dead_item_ids
index_vacuum_count
```

可以判断是否因为维护内存限制而反复做 index vacuum cycle。

### cost delay 是 I/O 影响控制，不是带宽保证

vacuum 给页面操作累计抽象 cost：

```text
page hit
page miss
page dirty
```

达到 `vacuum_cost_limit` 后，sleep `vacuum_cost_delay` 再继续。

autovacuum 对应：

```text
autovacuum_vacuum_cost_delay
autovacuum_vacuum_cost_limit
```

若 autovacuum cost limit 非 `-1`，PostgreSQL 会在并行 worker 之间按比例分配，使各
worker limit 合计不超过该值。这意味着：

> worker 变多，不等于每个 worker 都拿到完整 limit。

另外：

- 手工 `VACUUM` 的 cost delay 默认关闭，除非显式设置非零；
- 持有关键锁的操作段不会照常 sleep；
- failsafe 触发后会停止 cost delay，并跳过非必要工作来优先防回卷；
- cost unit 不是 IOPS 或 MB/s，必须用 OS/Pigsty I/O 指标校准。

正式实验为了可靠抓取进度，只在 vacuum session 设置：

```sql
SET vacuum_cost_delay = '10ms';
SET vacuum_cost_limit = 20;
SET track_cost_delay_timing = on;
```

session 结束即回退；没有修改集群配置。10 ms 是教学限速，不是推荐生产值。官方文档
指出正常配置通常应使用很小的 delay，大延迟并不理想。

### I/O 不是唯一竞争

vacuum 还会：

```text
read heap
dirty heap / VM / FSM
read and update indexes
generate WAL for maintenance changes
use CPU to evaluate tuple visibility
acquire relation/page locks
evict useful shared/OS cache pages
```

所以 “iowait 不高” 不能证明 vacuum 无影响。可能：

- 数据在 cache，竞争表现为 CPU 和 buffer churn；
- device 很快，竞争表现为 foreground tail；
- cloud storage queue 未映射为 host iowait；
- cost delay 让 worker 大量 sleep；
- checkpoint/backup 与 vacuum 交织。

### 维护优先级不是一刀切

可把对象分三层：

```text
P0 correctness:
  XID/MXID danger
  suspected corruption

P1 service health:
  dead tuple backlog accelerating
  index cleanup not completing
  table growth threatens disk/SLO

P2 efficiency:
  moderate bloat
  stale statistics
  low HOT ratio
```

P0 不能为了降低业务 I/O 无限限速；P2 不应在业务峰值争抢资源。

## 28.2.3 进度、阻塞与“为什么没清掉” {#item-28-2-3}

### 先确认 worker 身份

```sql
SELECT
  pid,
  datname,
  usename,
  backend_type,
  application_name,
  state,
  wait_event_type,
  wait_event,
  xact_start,
  query_start,
  query
FROM pg_stat_activity
WHERE backend_type = 'autovacuum worker'
   OR query LIKE 'autovacuum:%'
ORDER BY query_start;
```

防回卷 worker 的 query 文本会带 `(to prevent wraparound)`。它与普通 autovacuum 的
取消策略不同：冲突锁通常可中断普通 autovacuum，但防回卷 worker 不会被自动中断。

### 读取原生 progress

```sql
SELECT
  p.pid,
  p.datname,
  p.relid::regclass AS relation,
  p.phase,
  p.heap_blks_total,
  p.heap_blks_scanned,
  p.heap_blks_vacuumed,
  p.index_vacuum_count,
  p.dead_tuple_bytes,
  p.num_dead_item_ids,
  p.indexes_total,
  p.indexes_processed,
  p.delay_time
FROM pg_stat_progress_vacuum AS p
ORDER BY p.pid;
```

PostgreSQL 18 的主要 phase：

```text
initializing
scanning heap
vacuuming indexes
vacuuming heap
cleaning up indexes
truncating heap
performing final cleanup
```

解释时注意：

- `heap_blks_total` 是开始扫描时的规模；
- VM 跳过的块仍会计入 scanned 的推进；
- `heap_blks_vacuumed` 可能跳跃；
- index 可能有多个 cycle；
- truncation、锁等待和 index cleanup 的耗时不由 heap 扫描百分比线性预测。

所以：

```text
heap_blks_scanned / heap_blks_total
```

是 scan progress，不是可靠 ETA。

### “没清掉”的决策树

```text
Did VACUUM run?
├─ no
│  ├─ below threshold
│  ├─ worker unavailable
│  ├─ autovacuum/table option disabled
│  ├─ statistics not updating
│  └─ permissions/manual command skipped relation
└─ yes
   ├─ old snapshot still needs tuples
   ├─ replication slot xmin/catalog_xmin retains them
   ├─ prepared transaction retains horizon/locks
   ├─ index cleanup skipped/deferred
   ├─ pages skipped to avoid waits
   ├─ only estimate is stale
   ├─ space became reusable but file did not shrink
   └─ new churn arrived as fast as cleanup
```

### blocker inventory

```sql
SELECT
  pid,
  usename,
  application_name,
  state,
  xact_start,
  state_change,
  backend_xid,
  backend_xmin,
  age(backend_xid)  AS xid_age,
  age(backend_xmin) AS xmin_age,
  wait_event_type,
  wait_event
FROM pg_stat_activity
WHERE backend_xid IS NOT NULL
   OR backend_xmin IS NOT NULL
   OR state LIKE 'idle in transaction%'
ORDER BY age(backend_xmin) DESC NULLS LAST;
```

再查：

```sql
SELECT
  slot_name,
  slot_type,
  database,
  active,
  age(xmin) AS xmin_age,
  age(catalog_xmin) AS catalog_xmin_age,
  restart_lsn,
  wal_status,
  inactive_since,
  invalidation_reason
FROM pg_replication_slots;

SELECT
  transaction,
  age(transaction) AS xid_age,
  gid,
  prepared,
  owner,
  database
FROM pg_prepared_xacts
ORDER BY age(transaction) DESC;
```

不要在第一条查询里直接拼 `pg_terminate_backend`。先确认：

```text
owner
application
business transaction semantics
retry behavior
prepared transaction coordinator
replication consumer
HA/failover impact
```

然后才能决定 cancel、terminate、commit、rollback 或 drop slot。

### 累计结果

```sql
SELECT
  relid::regclass,
  n_live_tup,
  n_dead_tup,
  last_vacuum,
  last_autovacuum,
  vacuum_count,
  autovacuum_count,
  total_vacuum_time,
  total_autovacuum_time
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
```

PostgreSQL 18 增加/提供 vacuum/analyze 累计耗时列；部署跨版本查询时要先检查列存在。
累计 view 会 reset，必须联合 `stats_reset` 和 Pigsty 时序数据，不要把 reset 后的
“低计数”解释成改善。

### Pigsty：历史趋势与原生瞬时事实互补

Pigsty 的监控栈把 PostgreSQL、PgBouncer、Patroni、主机和日志放到同一组
`cls/ins/ip` 标签下。对维护问题，常用：

| 页面 | 看什么 |
|---|---|
| PGSQL Tables / Table | dead/live、scan、vacuum、relation trend |
| PGCAT Table | 当前 catalog、size、bloat 类诊断 |
| PGSQL Persist | XID、WAL、checkpoint、archive、持久性 |
| PGSQL Activity / Session | backend、wait、长事务 |
| PGSQL Replication | slot、replica、replay/retention |
| PGCAT Locks | blocker/waiter |
| PGLOG | autovacuum verbose、warning、cancel/failsafe |
| NODE Instance | disk latency、queue、space、CPU、memory |

Dashboard 回答：

```text
when did it start?
is it accelerating?
which instance/table changed?
what else happened at the same time?
```

原生 SQL 回答：

```text
which PID and phase now?
which exact xmin/slot/prepared xact retains horizon?
which reloption and effective GUC applies?
```

两者必须互证。Grafana panel 不是另一个数据库真相层。

### 处置顺序

```text
1. classify correctness vs service vs efficiency
2. verify actual trigger inputs and table overrides
3. locate active/queued workers and progress
4. inventory holders
5. compare cleanup rate with churn rate
6. check I/O/CPU/memory/WAL/replica side effects
7. choose smallest reversible intervention
8. validate dead/reusable/age outcome
9. record desired state and rollback
```

跳过第 4 步直接“手工再 vacuum 一次”，通常只会重复同一失败。

## 本节检查清单

```text
effective update/delete threshold
effective insert threshold
analyze threshold
freeze/MXID age
table reloptions
eligible backlog
worker slots / max workers
per-worker and total memory budget
cost limit distribution
progress phase and cycle
old snapshot/slot/2PC holders
foreground tail and device pressure
cleanup rate vs churn rate
```

## 延伸阅读

- [PostgreSQL 18：The Autovacuum Daemon](https://www.postgresql.org/docs/18/routine-vacuuming.html#AUTOVACUUM)
- [PostgreSQL 18：Vacuuming Configuration](https://www.postgresql.org/docs/18/runtime-config-vacuum.html)
- [PostgreSQL 18：VACUUM Progress Reporting](https://www.postgresql.org/docs/18/progress-reporting.html#VACUUM-PROGRESS-REPORTING)
- [PostgreSQL 18：`pg_stat_progress_vacuum`](https://www.postgresql.org/docs/18/progress-reporting.html)
- [Pigsty：PostgreSQL Monitoring](https://pigsty.io/docs/pgsql/monitor/)
- [Pigsty：PGSQL Dashboards](https://pigsty.io/docs/pgsql/dashboard/)

---

[上一节：死元组与可见性](../01/) · [返回本章目录](../) · [下一节：冻结、XID 与保留者](../03/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
