← [ BACK_TO_NOTES ]

BodySense 数据库第 17 课:VACUUM、Autovacuum、Bloat、ANALYZE 与 Planner Statistics

把 MVCC 产生的 dead tuples、VACUUM 回收、visibility map、XID wraparound、ANALYZE statistics 与查询计划放进同一 PostgreSQL 运维模型。

#course #growing #backend #database #postgresql #bodysense

[!info] related notes

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]

  1. UPDATE 一行 1000 次,表物理空间可能为什么增长?
  2. VACUUM 和 VACUUM FULL 的核心差异是什么?
  3. 查询计划突然变差,为什么 ANALYZE 值得检查?
  4. 为什么长期 idle in transaction 可能伤害 VACUUM?
  5. 可以为了减少 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。

创建于 2026/9/18 更新于 2026/9/18
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本