PostgreSQL VACUUM 清理機(jī)制詳解
一、為什么需要 VACUUM?
PostgreSQL 使用 MVCC(多版本并發(fā)控制)實現(xiàn)事務(wù)隔離:
- UPDATE 操作:本質(zhì)是 DELETE + INSERT,舊版本數(shù)據(jù)并不會立即刪除
- DELETE 操作:只是將數(shù)據(jù)標(biāo)記為"已刪除",物理空間不釋放
這導(dǎo)致大量死元組(dead tuples) 殘留在表中:
┌──────────────────────────────────────────────────────┐
│ 表空間 │
│ [活躍數(shù)據(jù)] [死元組] [活躍數(shù)據(jù)] [死元組] [死元組] │
│ │
│ 死元組累積 → 空間浪費(fèi) → 查詢變慢 → 需要 VACUUM │
└──────────────────────────────────────────────────────┘
VACUUM 就是負(fù)責(zé)回收這些死元組、釋放空間、更新統(tǒng)計信息的維護(hù)命令。
二、哪些操作會產(chǎn)生空間碎片?
2.1 高頻 UPDATE
每次 UPDATE 都會保留舊版本,舊版本變成死元組:
-- 訂單狀態(tài)每次變更,都產(chǎn)生一個舊版本死元組 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;
初始: Page 1: [Row-v1] [空閑] [空閑] [空閑]
3次UPDATE后:
Page 1: [Row-v1(死)] [Row-v2(死)] [Row-v3(死)] [Row-v4]
← 60% 空間被死元組占用
2.2 高頻 DELETE
大量刪除后,空間被死元組占據(jù)無法重用:
-- 每天刪除過期日志 DELETE FROM interface_execution_log WHERE start_time < now() - interval '90 days';
?? 即使刪除了 500 萬行,表文件大小也不會縮小,空間不會歸還給操作系統(tǒng)。
2.3 批量數(shù)據(jù)導(dǎo)入 + 清理
-- Step 1:導(dǎo)入 1000 萬行臨時數(shù)據(jù) INSERT INTO odh_sell_in_inbound SELECT * FROM external_source; -- Step 2:數(shù)據(jù)處理完畢,刪除臨時數(shù)據(jù) DELETE FROM odh_sell_in_inbound WHERE batch_id = 'xxx'; -- 結(jié)果:表大小維持在 1000 萬行的體量,內(nèi)部全是死元組空洞
2.4 長時間未提交的事務(wù)
-- 事務(wù) A 開啟但長時間未提交 BEGIN; SELECT * FROM lorder_master_info WHERE id = 1; -- ? 業(yè)務(wù)處理了很久,事務(wù)未提交... -- 此期間,其他事務(wù)產(chǎn)生的所有死元組都無法被 VACUUM 清理 -- 因為事務(wù) A 可能還需要讀到舊版本數(shù)據(jù)
?? 這是線上最常見的表膨脹根因之一,尤其是跑批任務(wù)或報表查詢時。
2.5 高并發(fā)小事務(wù)
-- 每秒上萬次庫存扣減 UPDATE inventory SET stock = stock - 1 WHERE product_id = 'HOT001';
短時間內(nèi)大量死元組堆積,查詢性能會急劇下降。
三、如何診斷表膨脹?
-- 查看死元組比例,找出需要清理的表
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;
判斷標(biāo)準(zhǔn):
| 死元組比例 | 狀態(tài) | 處理建議 |
|---|---|---|
| < 5% | ?? 健康 | 無需操作 |
| 5% ~ 20% | ?? 關(guān)注 | 執(zhí)行 VACUUM ANALYZE |
| > 20% | ?? 膨脹 | 立即執(zhí)行 VACUUM,嚴(yán)重時用 VACUUM FULL |
四、VACUUM 的類型與使用
4.1 VACUUM —— 日常清理(推薦)
-- 清理單表 VACUUM orders; -- 清理 + 更新統(tǒng)計信息(最常用) VACUUM ANALYZE orders; -- 查看清理詳情 VACUUM VERBOSE ANALYZE orders;
特點:
- ? 不鎖表,允許并發(fā)讀寫,可在線執(zhí)行
- ? 將死元組空間標(biāo)記為可重用(供后續(xù) INSERT/UPDATE 使用)
- ? 不歸還空間給操作系統(tǒng),表文件大小不變
4.2 VACUUM FULL —— 深度清理(維護(hù)窗口)
VACUUM FULL VERBOSE ANALYZE orders;
工作原理:
1. 創(chuàng)建新的表文件
2. 將所有活躍數(shù)據(jù)緊湊復(fù)制到新文件
3. 刪除舊文件,重建所有索引
4. ? 空間歸還給操作系統(tǒng),表文件大幅縮小
特點:
- ? 徹底回收空間,消除所有碎片
- ? 需要 ACCESS EXCLUSIVE 鎖,執(zhí)行期間表不可讀寫
- ? 需要約 2 倍表大小的臨時磁盤空間
- ? 大表耗時很長(GB 級別可能需要數(shù)十分鐘)
?? 僅在業(yè)務(wù)低峰期(如凌晨維護(hù)窗口)執(zhí)行,生產(chǎn)高峰期禁止使用。
4.3 VACUUM ANALYZE —— 清理 + 更新統(tǒng)計信息
統(tǒng)計信息過時會導(dǎo)致查詢優(yōu)化器選錯執(zhí)行計劃:
-- 數(shù)據(jù)大量變更后,一定要執(zhí)行 ANALYZE VACUUM ANALYZE lorder_master_info; -- 或只更新統(tǒng)計信息(不清理) ANALYZE lorder_master_info;
典型場景:
- 大批量數(shù)據(jù)導(dǎo)入后
- 創(chuàng)建新索引后
- 某張表數(shù)據(jù)量變化超過 20% 后
效果對比:
統(tǒng)計信息過時 → 優(yōu)化器估算:10 行 → 選 Nested Loop → 執(zhí)行 30 秒
執(zhí)行 ANALYZE → 優(yōu)化器估算:100 萬行 → 選 Hash Join → 執(zhí)行 2 秒
4.4 VACUUM FREEZE —— 防止事務(wù) ID 回繞
PostgreSQL 使用 32 位事務(wù) ID(XID),用完(約 42 億)后會回繞,導(dǎo)致數(shù)據(jù)混亂:
-- 查看各表的事務(wù)年齡(接近 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;
-- 出現(xiàn)以下告警時,立即執(zhí)行:
-- WARNING: database must be vacuumed within 1000000 transactions
VACUUM FREEZE;
五、VACUUM 的核心好處
5.1 回收空間,降低 I/O
清理前:表 100GB,有效數(shù)據(jù) 60GB,死元組 40GB → 全表掃描讀 100GB
VACUUM 后:死元組空間標(biāo)記為可重用,表不再無限膨脹
VACUUM FULL 后:表縮減為 60GB → 全表掃描只需讀 60GB,I/O 節(jié)省 40%
5.2 提升查詢性能
清理前:
Page 1: [Data][Dead][Dead][Data] ← 掃描效率 50%
Page 2: [Dead][Dead][Data][Dead]
Page 3: [Data][Data][Dead][Dead]
→ 掃描 3 頁,只有 50% 有效數(shù)據(jù)清理后(VACUUM FULL):
Page 1: [Data][Data][Data][Data] ← 掃描效率 100%
Page 2: [Data][Data][空閑][空閑]
→ 掃描 2 頁,100% 有效數(shù)據(jù),性能提升 ~60%
5.3 優(yōu)化查詢執(zhí)行計劃
統(tǒng)計信息過時是慢查詢的常見根因:
-- 統(tǒng)計信息過時 → 優(yōu)化器估算行數(shù)偏差巨大 → 選錯 Join 方式 → 慢 30 倍 -- VACUUM ANALYZE 后 → 統(tǒng)計準(zhǔn)確 → 選 Hash Join → 正常速度 VACUUM ANALYZE orders;
5.4 改善 Shared Buffer 緩存命中率
清理前:Buffer 中緩存大量死元組頁,熱數(shù)據(jù)被擠出
清理后:Buffer 中全是有效數(shù)據(jù),緩存命中率顯著提升
5.5 防止事務(wù) ID 回繞(數(shù)據(jù)庫崩潰風(fēng)險)
定期 VACUUM 會自動凍結(jié)舊事務(wù) ID,防止 XID 回繞導(dǎo)致數(shù)據(jù)庫不可用。
六、實戰(zhàn)清理操作指南
6.1 標(biāo)準(zhǔn)清理(不鎖表)
-- 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:執(zhí)行清理(不鎖表)
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 深度清理(維護(hù)窗口執(zhí)行)
-- Step 1:確認(rèn)磁盤空間(需要 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:設(shè)置超時保護(hù)
SET statement_timeout = '2h';
-- Step 3:執(zhí)行深度清理(?? 會鎖表)
VACUUM FULL VERBOSE ANALYZE orders;
6.3 分區(qū)表清理策略
針對項目中的大分區(qū)表,逐個分區(qū)清理,避免一次性影響范圍過大:
-- 逐個分區(qū)清理(推薦)
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;
-- 批量清理所有子分區(qū)
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 批量操作前后的最佳實踐
-- ? 大批量導(dǎo)入后,立即更新統(tǒng)計信息 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; -- ? 創(chuàng)建索引后,更新統(tǒng)計信息 CREATE INDEX CONCURRENTLY idx_xxx ON lorder_master_info (geo_type, fiscal_year); ANALYZE lorder_master_info;
6.5 在線清理方案(pg_repack)
VACUUM FULL 會鎖表,生產(chǎn)環(huán)境推薦用 pg_repack 替代:
# 安裝(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 倍表大小 |
| 生產(chǎn)適用 | ? 僅維護(hù)窗口 | ? 隨時可用 |
適用版本:PostgreSQL 12+
到此這篇關(guān)于PostgreSQL VACUUM 清理機(jī)制詳解的文章就介紹到這了,更多相關(guān)PostgreSQL VACUUM 清理機(jī)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
對PostgreSQL中的慢查詢進(jìn)行分析和優(yōu)化的操作指南
在數(shù)據(jù)庫的世界里,慢查詢就像是路上的絆腳石,讓數(shù)據(jù)處理的道路變得崎嶇不平,想象一下,你正在高速公路上飛馳,突然遇到一堆減速帶,那感覺肯定糟透了,本文介紹了怎樣對?PostgreSQL?中的慢查詢進(jìn)行分析和優(yōu)化,需要的朋友可以參考下2024-07-07
解決PostgreSQL服務(wù)啟動后占用100% CPU卡死的問題
前文書說到,今天耗費(fèi)了九牛二虎之力,終于馴服了NTFS權(quán)限安裝好了PostgreSQL,卻不曾想,服務(wù)啟動后,新的狀況又出現(xiàn)了。2009-08-08
基于PostgreSQL和mysql數(shù)據(jù)類型對比兼容
這篇文章主要介紹了基于PostgreSQL和mysql數(shù)據(jù)類型對比兼容,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
在PostgreSQL中將整數(shù)(int)轉(zhuǎn)換為字符串的方法匯總
PostgreSQL中將整數(shù)轉(zhuǎn)換為字符串有多種方法,包括使用CAST函數(shù)、::操作符、字符串連接、to_char()函數(shù)等,每種方法都有其適用場景和性能特點,推薦根據(jù)具體需求選擇合適的方法,需要的朋友可以參考下2025-12-12
詳解如何在PostgreSQL中使用JSON數(shù)據(jù)類型
JSON(JavaScript Object Notation)是一種輕量級的數(shù)據(jù)交換格式,它采用鍵值對的形式來表示數(shù)據(jù),支持多種數(shù)據(jù)類型,本文給大家介紹了如何在PostgreSQL中使用JSON數(shù)據(jù)類型,需要的朋友可以參考下2024-03-03

