BodySense 数据库第 17 课:VACUUM、Autovacuum、Bloat、ANALYZE 与 Planner Statistics
把 MVCC 产生的 dead tuples、VACUUM 回收、visibility map、XID wraparound、ANALYZE statistics 与查询计划放进同一 PostgreSQL 运维模型。
[!info] related notes
- 课程 MOC:BodySense 数据库工程深化课程 MOC
- 前置:第 16 课
- 知识:MVCC、EXPLAIN / ANALYZE
BodySense 数据库第 17 课:VACUUM、Autovacuum、Bloat、ANALYZE 与 Planner Statistics
[!abstract] 本课目标 VACUUM 不是“数据库碎片整理”。它同时连接 MVCC 空间回收、visibility、planner statistics 和 transaction ID 安全。
从零建立概念:为什么 PostgreSQL 需要持续“清理旧版本”
第 13 课已经知道:
UPDATE / DELETE
→ old tuple version 不会立即物理擦除
因为:
older snapshot
可能仍然需要看到它
等所有相关 transaction 都不再需要后,它才真正成为可回收的 dead tuple。
问题来了:
谁来回收?
答案就是 VACUUM 系统。
VACUUM 不是简单的“磁盘碎片整理”
标准 VACUUM 的核心职责包括:
让 dead tuple 占用空间可被后续 tuple 重用
维护 visibility map
推进/freeze transaction ID age
清理部分 index dead entries
与 Planner Statistics 相关的工作由 ANALYZE 完成;可以单独运行 ANALYZE,也可以使用 VACUUM (ANALYZE) 一起执行。
这比一句:
VACUUM = 清垃圾
更准确。
Space Reuse 与 Return-to-OS 是两件事
标准 VACUUM 后:
dead space
→ reusable inside relation
表文件大小:
不一定明显下降
因为空闲空间通常留在 relation 中供以后 INSERT/UPDATE 重用。
这也是为什么:
DELETE 90% rows
→ `du` / table size 没立刻缩 90%
并不表示 VACUUM 完全没工作。
VACUUM FULL 为什么不同
VACUUM FULL 会重写整张表到更紧凑的新物理文件。
好处:
可以真正把更多空间归还 OS
代价:
slow
extra temporary disk
ACCESS EXCLUSIVE lock
blocks normal table use
官方建议日常优先标准 VACUUM,而不是把 FULL 当默认维护命令。
Visibility Map:为什么它还能影响 Index-Only Scan
PostgreSQL 需要 MVCC visibility check。
即使 Index 已包含所有查询列:
index entry exists
数据库还要知道:
对这个 page 来说,tuple 是否已经对所有 transaction 都可见,不需要再去 Heap 验证?
Visibility Map 记录 page 的 all-visible / all-frozen 等信息。
VACUUM 会维护它。
于是:
more pages all-visible
→ Index Only Scan 更可能少做 Heap Fetch
这把:
MVCC
VACUUM
Index-Only Scan
连起来了。
ANALYZE:Planner 为什么需要统计而不是每次精确扫描
Planner 选择 Plan 前需要估算:
WHERE 会剩多少 rows?
JOIN 会产生多少 rows?
某列 distinct 有多少?
值分布是否 skew?
它不能每次先完整跑一遍 query 再决定怎么跑 query。
所以 PostgreSQL 保存统计信息。
ANALYZE 会采样数据并更新这些 statistics。
常见概念:
n_distinct
most common values
histogram
null fraction
correlation
extended statistics
如果统计陈旧:
estimated rows wildly wrong
→ planner chooses bad join/scan
所以一个“突然慢查询”可能不需要加索引,而是先查 statistics。
Autovacuum:为什么不能靠 DBA 想起来再手工执行
生产表持续变化。
如果要求人每天:
看看哪张表该 VACUUM
再看看哪张表该 ANALYZE
不可扩展也不可靠。
Autovacuum daemon 会根据表变化与 age 触发 worker,自动执行 vacuum/analyze 类维护。
默认配置对很多系统已经够用,但高 churn 大表可能需要 per-table tuning。
Scale Factor 为什么大表容易出现特殊问题
触发阈值通常与:
base threshold
+ table size × scale factor
相关。
对超大表:
scale factor 0.2
× 100 million rows
→ 需要非常多变化才触发
可能太晚。
所以高更新大表常需要更低 scale factor / 合适 threshold。
不要直接复制一组“最佳参数”,先量:
write rate
dead tuple growth
vacuum duration
I/O headroom
transaction age
Transaction ID Wraparound:VACUUM 还在保护正确性
PostgreSQL 普通 XID 核心空间是 32-bit,会循环。
如果旧 tuple 的 transaction ID 永远不处理:
非常旧
经过 wraparound
→ 数值关系可能像“未来 transaction”
这会破坏 visibility 判断。
PostgreSQL 使用 freezing 等机制,把足够老且确定对未来都可见的 row 标记为 frozen。
因此:
VACUUM 不只是性能维护,还承担防止 transaction ID wraparound 的正确性职责。
这也是为什么不能因为“VACUUM 有 I/O”就长期关掉 autovacuum。
Bloat 是什么
Bloat 可以理解成:
relation/index 实际占用比当前有效数据所需大很多,导致更多 page 和维护成本。
来源可能:
heavy UPDATE/DELETE
old snapshots blocking cleanup
poor vacuum tuning
index churn
fillfactor/workload mismatch
影响:
more disk
more buffer footprint
more pages scanned
longer vacuum
slower backup
Long Transaction 为什么是 Vacuum 的敌人
假设:
T1 begins at 10:00
keeps old snapshot
10:01 onward
millions of updates
很多 old versions:
T1 could still need them
→ cannot safely reclaim
所以排查 bloat 时一定看:
oldest active transaction
idle in transaction
long-running query
replication slot / standby constraints(场景相关)
不能只盯 Autovacuum Worker。
生产观测应该看什么
表级至少:
n_live_tup
n_dead_tup
last_vacuum / last_autovacuum
last_analyze / last_autoanalyze
vacuum/analyze counts
relation size
index size
transaction age
PostgreSQL 新版本还有更丰富的 progress/stat views。
最终目标不是:
n_dead_tup 必须永远 0
而是:
dead tuple 产生和回收是否处于稳定、可解释的循环。
1. Dead Tuple 从哪里来
PostgreSQL MVCC 下 UPDATE/DELETE 留下旧版本。旧版本在所有相关 snapshot 都不再需要后,才可以被 VACUUM 标记/回收供页面重用。
2. VACUUM 的几项职责
回收/重用 dead tuple 空间
维护 visibility map
帮助 index-only scan
冻结旧 transaction ID,防 wraparound
VACUUM FULL 会重写表并需要强锁,不应当成日常维护默认按钮。
3. ANALYZE
Planner 依赖统计:
distinct values
null fraction
most common values
histogram
extended statistics
统计失真时,SQL 和索引都没变,query plan 也可能恶化。
4. Autovacuum
Autovacuum 根据表变化和 transaction age 动态触发 VACUUM/ANALYZE。高更新热点表可能需要独立调参,而不是全库一个阈值机械套用。
5. Bloat 与 Long Transaction
如果长事务保留老 snapshot:
dead tuple 不能清
→ table/index bloat
→ 更多 page
→ query / vacuum cost 上升
Prediction
[!question]
- UPDATE 一行 1000 次,表物理空间可能为什么增长?
VACUUM和VACUUM FULL的核心差异是什么?- 查询计划突然变差,为什么
ANALYZE值得检查?- 为什么长期 idle in transaction 可能伤害 VACUUM?
- 可以为了减少 I/O 直接长期关闭 autovacuum 吗?
[!success]- Answer / 解析 1. 多版本 UPDATE 会留下旧 tuple,直到可安全回收;高 churn 会制造 dead tuple 和 bloat。
2. 普通 VACUUM 主要让空间在表内可重用并可与日常读写并行;FULL 重写整表以归还更多空间给 OS,但更慢且需要 ACCESS EXCLUSIVE。
3. Planner 的 row/selectivity estimate 依赖统计;数据分布改变而统计陈旧会导致错误 cost decision。
4. 老 snapshot 仍可能需要旧版本,VACUUM 无法越过它安全清理。
5. 不应。 除空间/统计问题外,还存在 transaction ID wraparound 的正确性风险。
Lab
选择一张高更新表:查看 n_live_tup、n_dead_tup、last_autovacuum/last_analyze、table/index size。制造一批 UPDATE 后再次观察,并用 EXPLAIN 比较统计更新前后估计。
L4 Gate
能解释一条真实 PostgreSQL 性能退化链:high churn/long tx → dead tuples/bloat/statistics → more pages/bad plan → latency。