Group (cost=0.43..600032.52 rows=107 width=22) (actual time=0.019..6437.286 rows=107.00 loops=1)
Output: content
Group Key: test1.content
Buffers: shared hit=6660031
-> Index Only Scan using test_idx on test.test1 (cost=0.43..572532.52 rows=11000001 width=22) (actual time=0.018..5664.125 rows=11000001.00 loops=1)
Output: content
Heap Fetches: 11000001
Index Searches: 1
Buffers: shared hit=6660031
Planning Time: 0.073 ms
Execution Time: 6437.376 ms
上面是vacuum full后的执行计划,下面是vacuum full前的执行计划
Group (cost=0.43..209778.68 rows=107 width=22) (actual time=0.055..1434.377 rows=107.00 loops=1)
Output: content
Group Key: test1.content
Buffers: shared hit=321 read=8628
-> Index Only Scan using test_idx on test.test1 (cost=0.43..184779.50 rows=9999671 width=22) (actual time=0.054..810.491 rows=10000000.00 loops=1)
Output: content
Heap Fetches: 0
Index Searches: 1
Buffers: shared hit=321 read=8628
Planning:
Buffers: shared hit=24
Planning Time: 0.129 ms
Execution Time: 1434.431 ms
为何会有如此差异
你观察到的两个执行计划差异非常典型,
VACUUM FULL 前后的性能差异主要源于数据物理存储方式的变化,尤其是 索引可见性(visibility map) 和 堆页(heap page)的清理状态。✅ 关键差异点对比
Table
| 指标 | VACUUM FULL 前 | VACUUM FULL 后 |
|---|---|---|
| Heap Fetches | 0 | 11,000,001 |
| Buffers (hit) | 321 | 6,660,031 |
| Execution Time | 1.4s | 6.4s |
| Rows Scanned | 10M | 11M |
🔍 根本原因分析
✅ VACUUM FULL 前:Heap Fetches = 0
- 说明 Index Only Scan 完全不需要访问堆页,因为 visibility map 标记所有元组为可见。
- 这是 最优的 Index Only Scan 行为,性能极高。
- 数据页和索引页都 没有被清理或重写,但 visibility map 是最新的。
❌ VACUUM FULL 后:Heap Fetches = 11,000,001
- 说明 每扫描一个索引项,都要回表一次,即 访问堆页确认可见性。
- 这是因为 VACUUM FULL 会重写整个表,导致:
- 所有元组的 TID(行物理位置)发生变化;
- visibility map 被清空或失效;
- 索引未被重建,但指向的堆页位置已变;
- 因此 Postgres 无法信任 visibility map,必须回表确认每一行是否可见。
🧪 总结一句话
VACUUM FULL 后,visibility map 失效,导致 Index Only Scan 退化成了“Index Scan + Heap Fetch”,性能暴跌。
✅ 如何验证
你可以运行以下 SQL 来确认 visibility map 状态:
sql
Copy
SELECT pg_visibility_map('test.test1'::regclass, 1);或者查看:
sql
Copy
SELECT relallvisible FROM pg_class WHERE relname = 'test1';relallvisible在 VACUUM FULL 后通常会变成 0;- 在
VACUUM(非 FULL)后,这个值应该接近实际页数。
✅ 如何修复
不需要再次
VACUUM FULL,只需:sql
Copy
VACUUM ANALYZE test.test1;这会让 PostgreSQL 重新构建 visibility map,恢复 Heap Fetches = 0 的高效状态。
✅ 建议
- 避免在生产环境频繁使用
VACUUM FULL,除非你真的需要回收磁盘空间; - 日常维护用
VACUUM+ANALYZE即可; - 如果你需要重建表(如膨胀严重),考虑使用
pg_repack或CLUSTER,它们对 visibility map 更友好。
如你愿意,我可以帮你写一个脚本,自动判断是否需要 VACUUM 或重建表。