vacuum后查询变快是因为清除了死元组,减少扫描时的无效行过滤开销;它不缩减表大小,但提升i/o效率、支持索引仅扫描、优化后续插入,并需配合analyze更新统计信息。
为什么 vacuum 后查询变快?不是“删数据”而是“清障碍”
因为 vacuum 并不直接让查询逻辑变短,而是移除了查询执行时必须绕开或跳过的“死元组”(dead tuples)。postgresql 的 mvcc 要求查询扫描页面时逐行检查可见性——哪怕某行已被 update 或 delete 标记为无效,它仍物理存在、仍占页空间、仍被扫描器读取。死元组越多,一次 select 扫描的无效行就越多,i/o 和 cpu 开销自然上升。
常见错误现象:表大小没变小,但 EXPLAIN ANALYZE 显示 Seq Scan 的 rows removed by filter 高得异常;或者 pg_stat_all_tables.n_dead_tup 持续 > 10% n_live_tup。
-
VACUUM清理后,页面内有效行更紧凑,相同 I/O 可读更多活数据 - 更新了可见性映射(VM),使仅索引扫描(Index Only Scan)能真正跳过堆页访问
- 更新了 FSM(Free Space Map),后续
INSERT更可能复用本地空闲空间,减少页面分裂和随机写
Navicat 里点一下“VACUUM”到底执行了什么?
Navicat 对 PostgreSQL 表右键 → “Maintenance” → “Vacuum”,默认发出的是不含参数的 VACUUM 命令,等价于:
VACUUM table_name;
它不会锁表(只加 ShareUpdateExclusiveLock,允许并发读写),但会做三件事:
- 扫描全表,识别所有对当前及历史事务均不可见的死元组
- 清理对应索引项(如
index_page中指向已删 TID 的条目) - 更新
pg_class.relpages、reltuples和pg_stat_all_tables统计值
注意:Navicat 不会自动触发 ANALYZE,除非你勾选 “Analyze table” 选项——而统计信息不准正是很多查询计划退化(比如该走索引却走全表)的根源。
为什么有时候 VACUUM 了也没变快?
典型原因不是命令没跑,而是清理不彻底或治标不治本:
- 表刚被大量
UPDATE,但n_dead_tup还没达到 autovacuum 触发阈值(默认50 + 0.2 × n_live_tup),手动VACUUM能见效;但若持续高频更新,必须调低autovacuum_vacuum_scale_factor - 用了
VACUUM FULL(Navicat 里叫 “Vacuum Full”)——它重写整个表、释放空间给操作系统,但会锁表、耗时长、需要双倍磁盘空间,且对查询加速无额外帮助,反而可能因重建索引引入短暂延迟 - 死元组集中在某些热页,而
VACUUM又受限于maintenance_work_mem,导致部分页未被处理(日志里出现skipping page due to high cost) - 查询本身依赖过期统计信息,而你只跑了
VACUUM没跑ANALYZE,规划器仍按旧分布选错执行计划
真正要盯住的两个监控指标
别只看 Navicat 的“执行成功”,重点查这两个字段:
SELECT relname, n_dead_tup, n_live_tup,
round(n_dead_tup * 100.0 / NULLIF(n_dead_tup + n_live_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY dead_pct DESC LIMIT 5;
如果 dead_pct > 10%,说明表已明显“亚健康”;若长期 > 30%,大概率已有性能毛刺。另一个关键点是 last_autovacuum 时间——如果它比 last_analyze 早很多,说明 autovacuum 在清理,但 analyze 没跟上,得检查 autovacuum_analyze_* 参数是否被压得太低。
复杂点在于:vacuum 效果不是线性的。一次清理可能只让查询快 5%,但连续三次未清理,可能让某次查询从 50ms 涨到 800ms——因为死元组堆积引发缓冲区竞争、预读失效、索引膨胀连锁反应。这个临界点很难提前预警,只能靠定期盯指标。











