VACUUM 清理
VACUUM 是 PostgreSQL 中用来回收死元组(dead tuple)占用空间、冻结老事务 ID、更新可见性映射(Visibility Map)的维护命令。PostgreSQL 的 MVCC 机制决定了 UPDATE 与 DELETE 不会原地修改数据,而是留下旧版本元组;这些元组必须由 VACUUM 清理,否则表会持续膨胀(bloat),最终拖慢扫描、浪费磁盘,甚至在极端情况下触发事务 ID 回卷(XID wraparound)导致数据库强制停机。
本节从命令与运维的角度讲 VACUUM:有哪些参数、怎么诊断膨胀、自动清理为什么没跑、什么情况必须手工介入、以及如何安全地在生产环境执行。MVCC 层面的机制细节请参考架构章节。
前置知识
阅读本节前,建议先了解:ANALYZE 统计更新
基础概念
| 概念 | 术语 | 说明 |
|---|---|---|
| 死元组 | Dead Tuple | 对所有事务都不可见的旧版本行,等待回收 |
| 普通清理 | Plain VACUUM | 标记空间可复用,不缩小文件,不阻塞读写 |
| 完全清理 | VACUUM FULL | 整表重写,归还空间给操作系统,持排他锁 |
| 冻结 | Freeze | 将元组 xmin 置为特殊值,使其永不参与回卷判断 |
| 膨胀 | Bloat | 死元组与可复用空间占比过高导致表体积虚大 |
| 可见性映射 | VM(Visibility Map) | 标记「整页全可见」,是仅索引扫描的前提 |
| 空闲空间映射 | FSM(Free Space Map) | 记录每页剩余空间,供插入时复用 |
| 自动清理 | Autovacuum | 后台守护进程,按阈值自动触发 |
| 清理阈值 | autovacuum_vacuum_threshold | 触发自动清理的死元组绝对下限,默认 50 |
| 缩放因子 | autovacuum_vacuum_scale_factor | 触发自动清理的死元组比例,默认 0.2 |
| 事务 ID 回卷 | XID Wraparound | XID 耗尽导致的保护性停机风险 |
| 冻结年龄 | age(relfrozenxid) | 表最老未冻结 XID 的距离,接近 20 亿需告警 |
| 清理进度 | pg_stat_progress_vacuum | 查看正在进行的清理进度 |
| 在线重整理 | pg_repack | 不持长时间排他锁的重整表方案 |
关于 VACUUM 需要先建立三个认识:
- 它不是「删除垃圾」,而是「让空间可复用」。 普通
VACUUM执行后文件大小往往不变,这是正常的。 - 它必须定期执行。 不执行的后果不是表变大而已,而是冻结停止、回卷风险累积。
- 它默认不阻塞业务。 普通
VACUUM只取ShareUpdateExclusiveLock,与读写兼容,可随时执行。
-- 查看表的死元组与膨胀概况
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_vacuum,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 5; relname | n_live_tup | n_dead_tup | dead_pct | last_vacuum | last_autovacuum
----------+------------+------------+----------+-----------------------------+-----------------------------
orders | 11880 | 4210 | 26.17 | 2026-09-24 09:12:44.318+08 | 2026-09-24 10:05:19.771+08
products | 2000 | 38 | 1.86 | | 2026-09-24 09:58:02.114+08
(2 rows)语法与用法
VACUUM [ ( option [, ...] ) ] [ table_and_columns [, ...] ]
-- 常用选项
FULL [ boolean ] -- 整表重写,归还空间(持排他锁)
FREEZE [ boolean ] -- 强制冻结所有可见元组
VERBOSE [ boolean ] -- 输出清理明细
ANALYZE [ boolean ] -- 同时收集统计信息
DISABLE_PAGE_SKIPPING -- 不做全可见页跳过(用于修复 VM 异常)
SKIP_LOCKED -- 跳过无法立即加锁的表(避免等待)
INDEX_CLEANUP { AUTO | ON | OFF } -- 是否清理索引
TRUNCATE [ boolean ] -- 是否尝试截断末尾空页
PARALLEL n -- 并行工作进程数(PostgreSQL 12+,仅普通 VACUUM)
PROCESS_MAIN [ boolean ] -- 是否处理主表(PostgreSQL 13+)
PROCESS_TOAST [ boolean ] -- 是否处理 TOAST 表(PostgreSQL 13+)-- 最小可运行示例:日常维护
VACUUM (VERBOSE, ANALYZE) orders;INFO: vacuuming "public.orders"
INFO: launched 1 parallel vacuum worker
INFO: scanned index "orders_pkey" to remove 4210 row versions
INFO: "orders": removed 4210 dead row versions in 96 pages
INFO: "orders": found 4210 removable, 11880 nonremovable row versions in 420 pages
DETAIL: 0 dead row versions cannot be removed yet.
INFO: "orders": truncated 24 pages from 420 to 396
INFO: analyzing "public.orders"-- 查看哪些表需要清理(含自动清理阈值对比)
SELECT
relname,
n_live_tup,
n_dead_tup,
50 + 0.2 * n_live_tup AS vacuum_threshold,
last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 0
ORDER BY n_dead_tup DESC;详细说明
VACUUM 的四种执行形态
不同参数组合解决不同问题,运维时必须区分:
| 形态 | 命令 | 空间归还 OS | 锁级别 | 典型用途 |
|---|---|---|---|---|
| 普通清理 | VACUUM | 否(仅截断尾部) | ShareUpdateExclusiveLock | 日常维护,防膨胀 |
| 清理 + 分析 | VACUUM ANALYZE | 否 | 同左 | 大批量变更后的标准动作 |
| 强制冻结 | VACUUM FREEZE | 否 | 同左 | 回卷告警时紧急降低冻结年龄 |
| 完全清理 | VACUUM FULL | 是 | AccessExclusiveLock | 表已严重膨胀且可接受停机 |
-- 日常:不阻塞业务,标记空间可复用
VACUUM orders;
-- 大批量导入/删除后:连带更新统计
VACUUM (ANALYZE, VERBOSE) orders;
-- 紧急:冻结老事务 ID
VACUUM (FREEZE, VERBOSE) orders;
-- 高风险:整表重写
VACUUM FULL orders;普通 VACUUM 与 VACUUM FULL 的差别必须牢记:
普通 VACUUM:
死元组 → 空间标记为「可复用」→ 文件不缩小
后续 INSERT 复用这些空间,表体积停止增长
VACUUM FULL:
新建一张表副本 → 把活元组搬过去 → 删除原表 → 重命名
文件真正缩小,但期间持 ACCESS EXCLUSIVE 锁,业务全面阻塞参数详解与资源控制
VACUUM 通过「代价延迟(cost-based delay)」控制对 I/O 的冲击:
SHOW vacuum_cost_delay;
SHOW vacuum_cost_limit;
SHOW maintenance_work_mem; vacuum_cost_delay
-------------------
2ms
(1 row)
vacuum_cost_limit
-------------------
200
(1 row)
maintenance_work_mem
----------------------
64MB
(1 row)含义是:清理过程中每累计消耗 vacuum_cost_limit 个代价点,就休眠 vacuum_cost_delay 毫秒。这能避免 VACUUM 挤占业务 I/O,但也让清理变慢。
-- 临时加速一次手工清理(仅当前会话有效)
SET vacuum_cost_delay = 0;
SET maintenance_work_mem = '512MB';
VACUUM (VERBOSE, ANALYZE) orders;并行清理可以显著提速:
-- 启动 4 个并行工作进程(仅普通 VACUUM 支持)
VACUUM (PARALLEL 4, VERBOSE) orders;注意:并行清理的加速来自「并行扫描索引」,主表扫描仍是单进程。若表上索引很少,提速有限。
SKIP_LOCKED 在多表批量清理时很有用:
-- 不等待被占用的表,直接跳过
VACUUM (SKIP_LOCKED, VERBOSE) orders;WARNING: skipping "orders" --- lock not available自动清理的触发条件
自动清理由 autovacuum 守护进程执行,触发条件是:
触发阈值 = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × reltuples
= 50 + 0.2 × 表当前行数SHOW autovacuum;
SHOW autovacuum_vacuum_threshold;
SHOW autovacuum_vacuum_scale_factor;
SHOW autovacuum_naptime; autovacuum
------------
on
(1 row)
autovacuum_vacuum_threshold
-----------------------------
50
(1 row)
autovacuum_vacuum_scale_factor
--------------------------------
0.2
(1 row)
autovacuum_naptime
--------------------
1min
(1 row)举例:一张 100 万行的表,需要约 50 + 0.2 × 1000000 = 200050 个死元组才会触发自动清理。这意味着大表在小比例删除后可能长期不清理,而 20% 的膨胀比例对大表已是巨大浪费。
-- 查看表的清理阈值与当前死元组数
SELECT
relname,
n_live_tup,
n_dead_tup,
50 + 0.2 * n_live_tup AS threshold,
CASE WHEN n_dead_tup > 50 + 0.2 * n_live_tup THEN '应触发' ELSE '未达阈值' END AS status
FROM pg_stat_user_tables
WHERE n_live_tup > 0
ORDER BY n_dead_tup DESC
LIMIT 5; relname | n_live_tup | n_dead_tup | threshold | status
----------+------------+------------+-----------+----------
orders | 998420 | 98210 | 199734 | 未达阈值
products | 2000 | 4210 | 450 | 应触发
(2 rows)对频繁更新的大表,推荐按表降低缩放因子:
-- 只对大表精细化调整
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_cost_delay = 0
);诊断膨胀:找到真正需要清理的表
死元组数只反映「未清理的旧版本」,不等于物理膨胀。要看真实膨胀率需要 pgstattuple 扩展或估算查询:
-- 安装扩展(需要超级用户)
CREATE EXTENSION IF NOT EXISTS pgstattuple;-- 精确统计:扫描全表,代价较高
SELECT
table_len,
tuple_count,
dead_tuple_count,
round(dead_tuple_len * 100.0 / greatest(table_len, 1), 2) AS dead_pct,
round(free_space * 100.0 / greatest(table_len, 1), 2) AS free_pct
FROM pgstattuple('orders'); table_len | tuple_count | dead_tuple_count | dead_pct | free_pct
-----------+-------------+------------------+----------+----------
34603008 | 11880 | 4210 | 18.12 | 0.00
(1 row)-- 快速估算膨胀(不扫全表,基于统计信息)
SELECT
relname,
pg_size_pretty(pg_relation_size(relid)) AS table_size,
n_live_tup,
n_dead_tup,
round(n_dead_tup * 100.0 / greatest(n_live_tup + n_dead_tup, 1), 2) AS est_bloat_pct
FROM pg_stat_user_tables
WHERE n_live_tup + n_dead_tup > 0
ORDER BY est_bloat_pct DESC
LIMIT 10; relname | table_size | n_live_tup | n_dead_tup | est_bloat_pct
----------+------------+------------+------------+---------------
orders | 33 MB | 11880 | 4210 | 26.17
products | 312 kB | 2000 | 38 | 1.86
(2 rows)autovacuum 为什么没跑:常见阻塞原因
自动清理不执行的典型原因有六类:
| 原因 | 判断方法 | 处置 |
|---|---|---|
| 未达阈值 | 对比 n_dead_tup 与阈值 | 降低 scale_factor 或手工 VACUUM |
| 长事务阻塞 | 查 pg_stat_activity 中 xact_start 很早的会话 | 终止或优化长事务 |
| 复制槽阻塞 | 查 pg_replication_slots 的 active = false | 删除无用槽或推进 confirmed_flush_lsn |
| 表被显式禁用 | 查 pg_class.reloptions 中 autovacuum_enabled | 改回 on |
| 工作进程不足 | 查 pg_stat_activity 中 autovacuum 数量 | 调大 autovacuum_max_workers |
| 清理成本限速过强 | 查 vacuum_cost_delay / vacuum_cost_limit | 适度降低延迟 |
-- 检查长事务(最容易导致清理无法推进)
SELECT pid, now() - xact_start AS xact_age, state, left(query, 60) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start
LIMIT 5; pid | xact_age | state | query
-------+-------------------+--------+-------------------------------------------------------------
10422 | 02:41:18.73211 | idle in transaction | SELECT * FROM orders WHERE id = 1
(1 row)-- 检查失效的复制槽
SELECT slot_name, active, restart_lsn, confirmed_flush_lsn
FROM pg_replication_slots
WHERE active = false; slot_name | active | restart_lsn | confirmed_flush_lsn
-----------+--------+-------------+---------------------
standby1 | f | 3/8A0002C8 | 3/8A0002C8
(1 row)-- 检查是否有表被禁用自动清理
SELECT relname, reloptions
FROM pg_class
WHERE reloptions IS NOT NULL AND relname IN ('orders', 'products', 'users'); relname | reloptions
---------+------------------------------
orders | {autovacuum_enabled=false}
(1 row)冻结与事务 ID 回卷
PostgreSQL 的事务 ID 是 32 位循环使用的,当一个数据库最老未冻结的事务距当前超过 20 亿时会触发保护性停机。VACUUM 的隐藏职责就是冻结老元组,推进 relfrozenxid。
-- 查看各库与各表的冻结年龄
SELECT datname, age(datfrozenxid) AS db_age FROM pg_database ORDER BY db_age DESC LIMIT 3; datname | db_age
-----------+----------
demo | 62104512
postgres | 42018873
template1 | 41839201
(1 row)-- 表级别:找出冻结年龄最大的表
SELECT
relname,
age(relfrozenxid) AS xid_age,
pg_size_pretty(pg_relation_size(relid)) AS size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r' AND n.nspname = 'public'
ORDER BY xid_age DESC
LIMIT 5; relname | xid_age | size
----------+----------+--------
orders | 62104000 | 33 MB
products | 62103000 | 312 kB
(2 rows)关键参数:
-- postgresql.conf
autovacuum_freeze_max_age = 200000000 # 超过此年龄强制触发 anti-wraparound 清理(2 亿)
vacuum_freeze_min_age = 50000000 # 小于此年龄的元组不冻结(5 千万)
vacuum_freeze_table_age = 150000000 # 超过此年龄触发完整冻结扫描(1.5 亿)
autovacuum_max_workers = 3 # autovacuum 工作进程上限-- 紧急处置:手工冻结(会在 age 高时消耗较多 I/O)
VACUUM (FREEZE, VERBOSE) orders;XID 回卷是停机级风险
当 age(datfrozenxid) 逼近 20 亿,PostgreSQL 会拒绝新事务并强制停机,只能以单用户模式清理。必须监控 age(datfrozenxid) 并提前告警,用 VACUUM (FREEZE) 或确保 autovacuum 正常运行来降低年龄。
观察清理进度
大表清理可能持续数分钟,pg_stat_progress_vacuum 可查看实时进度(PostgreSQL 12+):
SELECT
p.pid,
p.datname,
c.relname,
p.phase,
p.heap_blks_total,
p.heap_blks_scanned,
p.heap_blks_vacuumed,
round(p.heap_blks_scanned * 100.0 / greatest(p.heap_blks_total, 1), 2) AS scanned_pct
FROM pg_stat_progress_vacuum p
JOIN pg_class c ON c.oid = p.relid; pid | datname | relname | phase | heap_blks_total | heap_blks_scanned | heap_blks_vacuumed | scanned_pct
-------+---------+---------+------------------+-----------------+-------------------+--------------------+-------------
20118 | demo | orders | vacuuming indexes| 4224 | 4224 | 3802 | 100.00
(1 row)常见 phase 取值:
| phase | 含义 |
|---|---|
initializing | 准备阶段 |
scanning heap | 扫描堆表,检查死元组 |
vacuuming indexes | 清理索引中的死条目 |
vacuuming heap | 回收堆表空间 |
cleaning up indexes | 索引清理收尾 |
truncating heap | 截断表尾空页 |
performing final cleanup | 最终清理 |
VACUUM FULL 与 pg_repack 的取舍
VACUUM FULL 能真正缩小文件,但代价极高:
VACUUM FULL 的代价:
① 持 ACCESS EXCLUSIVE 锁 —— 期间所有读写阻塞
② 需要约等于表大小的额外磁盘空间
③ 中断后回滚成本高
④ 阻塞期间同样阻塞了 autovacuum 与其他维护-- 危险操作:先确认业务可停机,并用会话级 lock_timeout 兜底
SET lock_timeout = '3s';
VACUUM FULL orders;ERROR: canceling statement due to lock timeout-- 错误!VACUUM 不能在事务块中执行,无法用 BEGIN/COMMIT 控制提交时机
BEGIN;
VACUUM FULL orders;
-- ERROR: VACUUM cannot run inside a transaction block
COMMIT;更安全的替代方案是 pg_repack:它以触发器 + 日志的方式在线重整理,只在切换的瞬间持锁。
# pg_repack 需先安装扩展
psql -d demo -c "CREATE EXTENSION pg_repack"
# 在线重整单表(仅最后切换瞬间短暂持锁)
pg_repack -d demo -t ordersINFO: repacking table "public.orders"
INFO: rebuilding indexes
INFO: swapping table方案对比:
| 方案 | 锁影响 | 额外空间 | 是否需停机 | 适用场景 |
|---|---|---|---|---|
普通 VACUUM | 几乎无 | 无 | 否 | 日常维护 |
VACUUM FULL | 持续排他锁 | 约 1 倍表大小 | 是 | 小表、可停机窗口 |
pg_repack | 仅切换瞬间 | 约 1 倍表大小 | 否 | 大表在线重整 |
| 重建表切换 | 切换瞬间 | 约 1 倍表大小 | 短 | 需自定义转换逻辑时 |
实战示例
以下示例统一使用 demo 库。
1. 准备数据
CREATE DATABASE demo;
\c demo
CREATE TABLE products (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku text NOT NULL UNIQUE,
name text NOT NULL,
category text NOT NULL,
price numeric(10, 2) NOT NULL,
stock integer NOT NULL DEFAULT 0
);
CREATE TABLE orders (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no text NOT NULL,
product_id integer NOT NULL REFERENCES products (id),
qty integer NOT NULL,
amount numeric(12, 2) NOT NULL,
status text NOT NULL,
created_at timestamp NOT NULL DEFAULT now()
);
CREATE INDEX idx_orders_status ON orders (status);2. 造数据并制造死元组
INSERT INTO products (sku, name, category, price, stock)
SELECT
'SKU-' || lpad(g::text, 4, '0'),
'商品 ' || g,
(ARRAY['数码', '家居', '服饰', 'food', '美妆', '运动'])[1 + (g % 6)],
round((50 + random() * 450)::numeric, 2),
(random() * 500)::integer
FROM generate_series(1, 2000) AS g;
INSERT INTO orders (order_no, product_id, qty, amount, status)
SELECT
'ORD-' || lpad(g::text, 6, '0'),
1 + (g % 2000),
1 + (g % 5),
round((50 + random() * 450)::numeric, 2),
(ARRAY['paid', 'pending', 'cancelled', 'refunded'])[1 + (g % 4)]
FROM generate_series(1, 12000) AS g;
-- 制造死元组:大量 UPDATE 会留下旧版本
UPDATE orders SET status = 'paid' WHERE status = 'pending';
UPDATE orders SET amount = amount * 1.1 WHERE status = 'cancelled';3. 观察膨胀
SELECT
relname,
n_live_tup,
n_dead_tup,
pg_size_pretty(pg_relation_size(relid)) AS size,
last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'orders'; relname | n_live_tup | n_dead_tup | size | last_autovacuum
---------+------------+------------+--------+-----------------------------
orders | 12000 | 5976 | 1256 kB|
(1 row)4. 执行清理
VACUUM (VERBOSE, ANALYZE) orders;INFO: vacuuming "public.orders"
INFO: launched 1 parallel vacuum worker
INFO: scanned index "orders_pkey" to remove 5976 row versions
INFO: scanned index "idx_orders_status" to remove 5976 row versions
INFO: "orders": removed 5976 dead row versions in 82 pages
INFO: "orders": found 5976 removable, 12000 nonremovable row versions in 157 pages
DETAIL: 0 dead row versions cannot be removed yet.
INFO: "orders": truncated 0 pages from 157 to 157
INFO: analyzing "public.orders"truncated 0 pages 说明表尾没有空页,文件大小不变 —— 这正是普通 VACUUM 的正常表现。
-- 死元组已归零,但文件大小基本不变
SELECT
relname,
n_dead_tup,
pg_size_pretty(pg_relation_size(relid)) AS size
FROM pg_stat_user_tables
WHERE relname = 'orders'; relname | n_dead_tup | size
---------+------------+--------
orders | 0 | 1256 kB
(1 row)5. 对比 VACUUM FULL 的空间归还
-- 记录清理前大小
SELECT pg_size_pretty(pg_total_relation_size('orders')) AS before_full; before_full
-------------
2232 kB
(1 row)-- 注意:持排他锁,生产环境需评估
VACUUM FULL orders;VACUUM-- 空间已归还
SELECT pg_size_pretty(pg_total_relation_size('orders')) AS after_full; after_full
------------
1216 kB
(1 row)6. 检查冻结年龄与自动清理状态
SELECT
relname,
age(relfrozenxid) AS xid_age,
n_dead_tup,
50 + 0.2 * n_live_tup AS threshold,
last_autovacuum
FROM pg_class c
JOIN pg_stat_user_tables s ON s.relid = c.oid
WHERE relname = 'orders'; relname | xid_age | n_dead_tup | threshold | last_autovacuum
---------+---------+------------+-----------+-----------------------------
orders | 62104810| 0 | 24050.0 | 2026-09-24 10:41:02.882+08
(1 row)-- 库级冻结年龄,建议纳入监控
SELECT datname, age(datfrozenxid) AS db_xid_age
FROM pg_database
WHERE datname = 'demo'; datname | db_xid_age
---------+------------
demo | 62104810
(1 row)注意事项
- 普通
VACUUM不会缩小表文件。 它只是让空间可复用,看到文件大小不变不要以为没生效。
-- 错误理解:跑完 VACUUM 表就变小了
VACUUM orders; -- 文件大小通常不变
-- 需要真正缩小时用 VACUUM FULL(有代价)或 pg_repackVACUUM FULL会阻塞所有读写。 不要在日常维护脚本里无条件使用。
-- 不推荐:直接在业务高峰执行
VACUUM FULL orders;
-- 推荐:加锁超时兜底,避免无限等待
SET lock_timeout = '3s';
VACUUM FULL orders;VACUUM不能在事务块中执行。
-- 错误!VACUUM 不能出现在事务块内
BEGIN;
VACUUM orders;
COMMIT;
-- 错误信息
-- ERROR: VACUUM cannot run inside a transaction blockautovacuum = off是极危险的配置。 关闭后死元组永不回收、冻结永不推进,最终导致回卷停机。
-- 错误!除非有完整的手工维护流程,否则不要关闭
ALTER SYSTEM SET autovacuum = off;
-- 正确:保持开启,只做精细化参数调整
ALTER TABLE orders SET (autovacuum_vacuum_scale_factor = 0.02);- 长事务是清理的最大敌人。 一个持续数小时的空闲事务会阻止所有更早死元组的回收。
-- 排查并处置长时间 idle in transaction 的会话
SELECT pid, now() - xact_start AS age, state
FROM pg_stat_activity
WHERE state = 'idle in transaction' AND now() - xact_start > interval '5 minutes';- 不要因为「表变大」就立刻
VACUUM FULL。 先确认是死元组膨胀,还是正常数据增长;后者重写表毫无意义。
-- 先诊断,再决定
SELECT n_live_tup, n_dead_tup,
round(n_dead_tup * 100.0 / greatest(n_live_tup + n_dead_tup, 1), 2) AS dead_pct
FROM pg_stat_user_tables WHERE relname = 'orders';VACUUM不更新统计信息。 需要统计信息时显式加ANALYZE,两者是不同的任务。
-- 只要统计
ANALYZE orders;
-- 两者都要
VACUUM ANALYZE orders;- 禁用表的 autovacuum 前必须有替代方案。
autovacuum_enabled = false常见的「优化」其实是把风险推迟到未来。
-- 不推荐:禁用后必须自己保证定期手工清理与冻结
ALTER TABLE orders SET (autovacuum_enabled = false);VACUUM FULL期间不可中断回滚廉价。 中断会丢弃已完成的重写,代价与重新执行接近,且期间一直阻塞。复制槽会「钉住」xmin 使清理停滞。 无消费者却处于非激活状态的槽必须及时清理。
-- 检查长期非激活的槽
SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots
WHERE active = false;最佳实践
- 保持 autovacuum 开启,并优先做「按表精细化调参」而非全局关闭。
-- 高频更新的大表:降低缩放因子、去掉清理延迟
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_cost_delay = 0
);- 把自动清理的状态纳入监控,关注
n_dead_tup比例与last_autovacuum时间。
SELECT
relname,
n_live_tup,
n_dead_tup,
last_autovacuum,
last_autoanalyze
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;- 监控事务 ID 年龄,把它当成一级告警指标。
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;-- 建议阈值:超过 10 亿告警,超过 15 亿必须处置
autovacuum_freeze_max_age = 200000000- 大批量
DELETE/UPDATE之后立即手工VACUUM ANALYZE,不要等自动清理。
DELETE FROM orders WHERE created_at < now() - interval '1 year';
VACUUM (ANALYZE, VERBOSE) orders;- 用
SKIP_LOCKED做批量维护,避免被个别繁忙表拖住整个脚本。
VACUUM (SKIP_LOCKED, ANALYZE, VERBOSE) orders;- 需要真正回收大表空间时优先选
pg_repack,而不是VACUUM FULL。
pg_repack -d demo -t orders --no-order- 定期检查并清理失效的复制槽与长事务,这两者是清理停滞最常见的根因。
SELECT pid, now() - xact_start AS age, state, left(query, 50) AS query
FROM pg_stat_activity
WHERE xact_start < now() - interval '10 minutes';- 在维护脚本中设置
lock_timeout,让VACUUM FULL等在拿不到锁时快速失败而不是排队阻塞后续业务。
SET lock_timeout = '3s';
SET statement_timeout = '30min';
VACUUM FULL orders;- 用
pg_stat_progress_vacuum观察长任务,并把heap_blks_total与扫描进度一起记录,便于判断是否需要干预。
SELECT pid, relid::regclass, phase, heap_blks_scanned, heap_blks_total
FROM pg_stat_progress_vacuum;下一节
继续学习:内连接 INNER JOIN