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] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路2 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x3 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn4 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈5 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780516 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123456 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒6 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记6 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598786 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端