PostgreSQL VACUUM 清理机制详解

PostgreSQL VACUUM 清理机制详解

一、为什么需要 VACUUM?

PostgreSQL 使用 MVCC(多版本并发控制)实现事务隔离:

  • UPDATE 操作:本质是 DELETE + INSERT,旧版本数据并不会立即删除
  • DELETE 操作:只是将数据标记为"已删除",物理空间不释放

这导致大量死元组(dead tuples) 残留在表中:

css 复制代码
┌──────────────────────────────────────────────────────┐
│  表空间                                              │
│  [活跃数据] [死元组] [活跃数据] [死元组] [死元组]   │
│                                                      │
│  死元组累积 → 空间浪费 → 查询变慢 → 需要 VACUUM    │
└──────────────────────────────────────────────────────┘

VACUUM 就是负责回收这些死元组、释放空间、更新统计信息的维护命令。


二、哪些操作会产生空间碎片?

2.1 高频 UPDATE

每次 UPDATE 都会保留旧版本,旧版本变成死元组:

sql 复制代码
-- 订单状态每次变更,都产生一个旧版本死元组
UPDATE orders SET status = 'PAID'      WHERE order_id = 12345;
UPDATE orders SET status = 'SHIPPED'   WHERE order_id = 12345;
UPDATE orders SET status = 'DELIVERED' WHERE order_id = 12345;
ini 复制代码
初始:  Page 1: [Row-v1] [空闲] [空闲] [空闲]
3次UPDATE后:
        Page 1: [Row-v1(死)] [Row-v2(死)] [Row-v3(死)] [Row-v4]
                ← 60% 空间被死元组占用

2.2 高频 DELETE

大量删除后,空间被死元组占据无法重用:

sql 复制代码
-- 每天删除过期日志
DELETE FROM interface_execution_log WHERE start_time < now() - interval '90 days';

⚠️ 即使删除了 500 万行,表文件大小也不会缩小,空间不会归还给操作系统。

2.3 批量数据导入 + 清理

sql 复制代码
-- Step 1:导入 1000 万行临时数据
INSERT INTO odh_sell_in_inbound SELECT * FROM external_source;

-- Step 2:数据处理完毕,删除临时数据
DELETE FROM odh_sell_in_inbound WHERE batch_id = 'xxx';

-- 结果:表大小维持在 1000 万行的体量,内部全是死元组空洞

2.4 长时间未提交的事务

sql 复制代码
-- 事务 A 开启但长时间未提交
BEGIN;
SELECT * FROM lorder_master_info WHERE id = 1;
-- ⏰ 业务处理了很久,事务未提交...

-- 此期间,其他事务产生的所有死元组都无法被 VACUUM 清理
-- 因为事务 A 可能还需要读到旧版本数据

⚠️ 这是线上最常见的表膨胀根因之一,尤其是跑批任务或报表查询时。

2.5 高并发小事务

sql 复制代码
-- 每秒上万次库存扣减
UPDATE inventory SET stock = stock - 1 WHERE product_id = 'HOT001';

短时间内大量死元组堆积,查询性能会急剧下降。


三、如何诊断表膨胀?

sql 复制代码
-- 查看死元组比例,找出需要清理的表
SELECT
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size,
    n_live_tup                                                          AS live_tuples,
    n_dead_tup                                                          AS dead_tuples,
    ROUND(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio,
    last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC
LIMIT 20;

判断标准:

死元组比例 状态 处理建议
< 5% 🟢 健康 无需操作
5% ~ 20% 🟡 关注 执行 VACUUM ANALYZE
> 20% 🔴 膨胀 立即执行 VACUUM,严重时用 VACUUM FULL

四、VACUUM 的类型与使用

4.1 VACUUM ------ 日常清理(推荐)

sql 复制代码
-- 清理单表
VACUUM orders;

-- 清理 + 更新统计信息(最常用)
VACUUM ANALYZE orders;

-- 查看清理详情
VACUUM VERBOSE ANALYZE orders;

特点:

  • 不锁表,允许并发读写,可在线执行
  • ✅ 将死元组空间标记为可重用(供后续 INSERT/UPDATE 使用)
  • ❌ 不归还空间给操作系统,表文件大小不变

4.2 VACUUM FULL ------ 深度清理(维护窗口)

sql 复制代码
VACUUM FULL VERBOSE ANALYZE orders;

工作原理:

markdown 复制代码
1. 创建新的表文件
2. 将所有活跃数据紧凑复制到新文件
3. 删除旧文件,重建所有索引
4. ✅ 空间归还给操作系统,表文件大幅缩小

特点:

  • ✅ 彻底回收空间,消除所有碎片
  • 需要 ACCESS EXCLUSIVE 锁,执行期间表不可读写
  • ❌ 需要约 2 倍表大小的临时磁盘空间
  • ❌ 大表耗时很长(GB 级别可能需要数十分钟)

⚠️ 仅在业务低峰期(如凌晨维护窗口)执行,生产高峰期禁止使用。

4.3 VACUUM ANALYZE ------ 清理 + 更新统计信息

统计信息过时会导致查询优化器选错执行计划:

sql 复制代码
-- 数据大量变更后,一定要执行 ANALYZE
VACUUM ANALYZE lorder_master_info;

-- 或只更新统计信息(不清理)
ANALYZE lorder_master_info;

典型场景:

  • 大批量数据导入后
  • 创建新索引后
  • 某张表数据量变化超过 20% 后

效果对比:

vbnet 复制代码
统计信息过时 → 优化器估算:10 行 → 选 Nested Loop → 执行 30 秒
执行 ANALYZE  → 优化器估算:100 万行 → 选 Hash Join → 执行 2 秒

4.4 VACUUM FREEZE ------ 防止事务 ID 回绕

PostgreSQL 使用 32 位事务 ID(XID),用完(约 42 亿)后会回绕,导致数据混乱:

sql 复制代码
-- 查看各表的事务年龄(接近 2 亿时需警惕)
SELECT
    relname,
    age(relfrozenxid) AS xid_age,
    pg_size_pretty(pg_total_relation_size(oid)) AS size
FROM pg_class
WHERE relkind = 'r'
ORDER BY age(relfrozenxid) DESC
LIMIT 10;

-- 出现以下告警时,立即执行:
-- WARNING: database must be vacuumed within 1000000 transactions
VACUUM FREEZE;

五、VACUUM 的核心好处

5.1 回收空间,降低 I/O

css 复制代码
清理前:表 100GB,有效数据 60GB,死元组 40GB → 全表扫描读 100GB

VACUUM 后:死元组空间标记为可重用,表不再无限膨胀

VACUUM FULL 后:表缩减为 60GB → 全表扫描只需读 60GB,I/O 节省 40%

5.2 提升查询性能

ini 复制代码
清理前:
  Page 1: [Data][Dead][Dead][Data]   ← 扫描效率 50%
  Page 2: [Dead][Dead][Data][Dead]
  Page 3: [Data][Data][Dead][Dead]
  → 扫描 3 页,只有 50% 有效数据

清理后(VACUUM FULL):
  Page 1: [Data][Data][Data][Data]   ← 扫描效率 100%
  Page 2: [Data][Data][空闲][空闲]
  → 扫描 2 页,100% 有效数据,性能提升 ~60%

5.3 优化查询执行计划

统计信息过时是慢查询的常见根因:

sql 复制代码
-- 统计信息过时 → 优化器估算行数偏差巨大 → 选错 Join 方式 → 慢 30 倍
-- VACUUM ANALYZE 后 → 统计准确 → 选 Hash Join → 正常速度
VACUUM ANALYZE orders;

5.4 改善 Shared Buffer 缓存命中率

清理前:Buffer 中缓存大量死元组页,热数据被挤出

清理后:Buffer 中全是有效数据,缓存命中率显著提升

5.5 防止事务 ID 回绕(数据库崩溃风险)

定期 VACUUM 会自动冻结旧事务 ID,防止 XID 回绕导致数据库不可用。


六、实战清理操作指南

6.1 标准清理(不锁表)

sql 复制代码
-- Step 1:找出需要清理的表
SELECT tablename, n_dead_tup,
       ROUND(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio
FROM pg_stat_user_tables
WHERE n_dead_tup > 100000
   OR (n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0)) > 20
ORDER BY n_dead_tup DESC;

-- Step 2:执行清理(不锁表)
VACUUM VERBOSE ANALYZE lorder_master_info;

-- Step 3:验证效果
SELECT pg_size_pretty(pg_total_relation_size('lorder_master_info')) AS size,
       n_dead_tup, last_vacuum
FROM pg_stat_user_tables
WHERE tablename = 'lorder_master_info';

6.2 深度清理(维护窗口执行)

sql 复制代码
-- Step 1:确认磁盘空间(需要 2 倍表大小)
SELECT pg_size_pretty(pg_total_relation_size('orders'))     AS current_size,
       pg_size_pretty(pg_total_relation_size('orders') * 2) AS required_space;

-- Step 2:设置超时保护
SET statement_timeout = '2h';

-- Step 3:执行深度清理(⚠️ 会锁表)
VACUUM FULL VERBOSE ANALYZE orders;

6.3 分区表清理策略

针对项目中的大分区表,逐个分区清理,避免一次性影响范围过大:

sql 复制代码
-- 逐个分区清理(推荐)
VACUUM VERBOSE ANALYZE lorder_master_info_ap_st_fy2526_q1;
VACUUM VERBOSE ANALYZE lorder_master_info_ap_st_fy2526_q2;
VACUUM VERBOSE ANALYZE lorder_master_info_ap_st_fy2526_q3;
VACUUM VERBOSE ANALYZE lorder_master_info_ap_st_fy2526_q4;

-- 批量清理所有子分区
DO $$
DECLARE r RECORD;
BEGIN
    FOR r IN
        SELECT tablename FROM pg_tables
        WHERE schemaname = 'public'
          AND tablename LIKE 'lorder_master_info_%'
    LOOP
        RAISE NOTICE '正在清理: %', r.tablename;
        EXECUTE 'VACUUM VERBOSE ANALYZE ' || quote_ident(r.tablename);
    END LOOP;
END $$;

6.4 批量操作前后的最佳实践

sql 复制代码
-- ✅ 大批量导入后,立即更新统计信息
INSERT INTO odh_sell_in_inbound SELECT * FROM staging_table;
VACUUM ANALYZE odh_sell_in_inbound;

-- ✅ 大批量删除后,回收死元组空间
DELETE FROM interface_execution_log WHERE start_time < now() - interval '90 days';
VACUUM interface_execution_log;

-- ✅ 创建索引后,更新统计信息
CREATE INDEX CONCURRENTLY idx_xxx ON lorder_master_info (geo_type, fiscal_year);
ANALYZE lorder_master_info;

6.5 在线清理方案(pg_repack)

VACUUM FULL 会锁表,生产环境推荐用 pg_repack 替代:

bash 复制代码
# 安装(Ubuntu)
sudo apt-get install postgresql-16-repack

# 在线整理表,不锁表,允许读写
pg_repack -d mydb -t orders
pg_repack -d mydb -t lorder_master_info_ap_st_fy2526_q4
对比项 VACUUM FULL pg_repack
锁表 ✅ 锁表(不可读写) ❌ 不锁表
空间回收 ✅ 完全回收 ✅ 完全回收
磁盘需求 2 倍表大小 2 倍表大小
生产适用 ❌ 仅维护窗口 ✅ 随时可用

适用版本:PostgreSQL 12+

相关推荐
折哥的程序人生 · 物流技术专研2 小时前
《Java面试85题图解版(二)》进阶深化中篇:Spring核心 + 数据库进阶
java·后端·spring·面试
TeamDev2 小时前
在 Excel 加载项中嵌入 Web 视图
前端·后端·.net
Mr_愚人派2 小时前
redis_点评详解(02.短信登录-验证码登录注册)
后端
Xidaoapi2 小时前
5分钟让你的Python项目接入GPT-4:从配置到上线的完整指南
后端
SamDeepThinking2 小时前
写代码不考虑前后兼容,迟早要还的
java·后端·程序员
庞轩px3 小时前
第四篇:SpringBoot自动配置——约定大于配置的底层原理
java·spring boot·后端·spring·自动配置·注解开发
追逐时光者3 小时前
C#/.NET/.NET Core技术前沿周刊 | 第 70 期(2026年5.01-5.10)
后端·.net
程序员清风3 小时前
科普一下:大模型Token的收费逻辑!
java·后端·面试
Nyarlathotep01133 小时前
并发集合类(4):ArrayBlockingQueue
java·后端