VACUUM と Bloat 対策 ── Dead Tuple との戦い
このレイヤーの役割:MVCC の副産物である Dead Tuple を回収し、テーブルの肥大化(Bloat)を防ぐ。PostgreSQL 運用の最重要トピック。

この章で何ができるようになるか:VACUUM の動作原理と autovacuum の設定を理解し、Bloat の監視と対策ができるようになる。
なぜ VACUUM が必要か
Ch.6(MVCC)で見たように、UPDATE は「旧タプルに削除マーク + 新タプル挿入」として動作する。旧タプル(Dead Tuple)はどのトランザクションからも見えなくなるが、物理的にはヒープに残り続ける。
Dead Tuple が蓄積すると:
1. テーブルサイズが際限なく肥大化
2. Seq Scan が遅くなる(Dead Tuple も物理的にスキャンする)
3. インデックスが肥大化(Dead Tuple を指すエントリが残る)
4. Visibility Map が更新されない → Index Only Scan が効かない
VACUUM の動作
VACUUM(通常):
1. テーブルの各ページをスキャン
2. Dead Tuple を特定(どの TX からも見えないタプル)
3. Dead Tuple が占めていた空間を「再利用可能」としてマーク(FSM に登録)
4. 対応するインデックスエントリを削除
5. Visibility Map を更新
6. ★ テーブルファイルのサイズは縮まらない(空き領域が再利用されるだけ)
VACUUM FULL:
1. テーブル全体を新しいファイルにコピー(Live Tuple だけ)
2. 旧ファイルを削除
3. ★ テーブルサイズが実際に縮む
4. ★ ACCESS EXCLUSIVE ロック(テーブルが完全にロックされる)
→ 本番環境では基本的に使わない
autovacuum:自動 VACUUM
-- autovacuum の基本パラメータ
SHOW autovacuum; -- on(デフォルト)
SHOW autovacuum_vacuum_threshold; -- 50(最低 Dead Tuple 数)
SHOW autovacuum_vacuum_scale_factor; -- 0.2(テーブルの 20%)
SHOW autovacuum_naptime; -- 1min(autovacuum の起動間隔)
-- autovacuum が発動する条件:
-- Dead Tuples > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × reltuples
-- 例: 100万行のテーブルでは Dead Tuples > 50 + 0.2 × 1000000 = 200050
テーブルごとのカスタマイズ
-- 更新が激しいテーブルは、より頻繁に VACUUM を走らせる
ALTER TABLE hot_table SET (
autovacuum_vacuum_scale_factor = 0.01, -- 1% で発動(デフォルト 20%)
autovacuum_vacuum_threshold = 100,
autovacuum_vacuum_cost_delay = 2 -- より積極的に VACUUM
);
-- ほぼ更新されないテーブルは、autovacuum を緩める
ALTER TABLE archive_table SET (
autovacuum_vacuum_scale_factor = 0.5, -- 50% まで待つ
autovacuum_enabled = false -- 完全に無効化(慎重に)
);
Transaction ID Wraparound:VACUUM を怠ると起きる大惨事
PostgreSQL の Transaction ID は32ビット(約42億)で、周回する。
TX ID の空間: 0 〜 4,294,967,295(約42億)
問題:
TX ID 100 で INSERT されたタプルが、TX ID 2,147,483,748 を超えた TX から
「未来の TX が INSERT した」と解釈される(周回)
→ データが突然見えなくなる
VACUUM の責務:
「凍結(Freeze)」— 古いタプルの t_xmin を特殊な値 FrozenXid に書き換える
→ どの TX からも常に「可視」になる
→ TX ID の周回に影響されなくなる
autovacuum_freeze_max_age = 200000000(デフォルト)
→ 2億トランザクション経過したら強制的に VACUUM Freeze が走る
-- テーブルごとの Freeze 状態を確認
SELECT
relname,
age(relfrozenxid) AS xid_age,
pg_size_pretty(pg_relation_size(oid)) AS size
FROM pg_class
WHERE relkind = 'r'
ORDER BY age(relfrozenxid) DESC
LIMIT 10;
-- xid_age が autovacuum_freeze_max_age に近づいたら警告
Bloat の監視
-- pgstattuple 拡張で Bloat を測定
CREATE EXTENSION pgstattuple;
SELECT
table_len,
tuple_count,
dead_tuple_count,
dead_tuple_percent,
free_space,
free_percent
FROM pgstattuple('users');
-- dead_tuple_percent が 20% を超えたら VACUUM が追いついていない
-- テーブルの実サイズ vs 理論的な最小サイズ
SELECT
relname,
pg_size_pretty(pg_relation_size(relid)) AS actual_size,
pg_size_pretty(
(reltuples * (SELECT avg(pg_column_size(t.*)) FROM users t LIMIT 1000))::bigint
) AS estimated_min_size
FROM pg_stat_user_tables
ORDER BY pg_relation_size(relid) DESC;
Bloat を縮小する方法(VACUUM FULL を使わずに)
pg_repack
テーブルを裏でリビルドし、短時間のロックで切り替える(VACUUM FULL の安全な代替)。
# pg_repack の実行(オンラインで実行可能)
pg_repack --table users --no-order --jobs 4 mydb
# → テーブル全体を新しいファイルにコピー
# → 最後に短時間のロック(ミリ秒〜秒)で切り替え
# → インデックスも再構築
CLUSTER
指定したインデックスの順序でテーブルを物理的に並べ替える(ACCESS EXCLUSIVE ロック)。
CLUSTER users USING idx_users_created_at;
-- → created_at 順に物理的にソート → 範囲検索が高速化
-- → ただし ACCESS EXCLUSIVE ロックが必要
autovacuum のトラブルシューティング
-- autovacuum が動いているか確認
SELECT
relname,
last_vacuum,
last_autovacuum,
n_dead_tup,
n_live_tup,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;
-- autovacuum が遅い原因の確認
SELECT
pid,
query,
state,
backend_xid,
backend_xmin -- ← これが古いと VACUUM が Dead Tuple を回収できない
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY age(backend_xmin) DESC
LIMIT 5;
-- → 長時間実行中の TX が Dead Tuple の回収を妨げている
最大の敵:長時間トランザクション
長時間トランザクション(例: 何時間も開いたまま放置された TX)があると、
その TX の開始時点より後に DELETE された Dead Tuple を VACUUM が回収できない。
→ Dead Tuple が際限なく蓄積 → Bloat → 性能劣化
対策:
- idle_in_transaction_session_timeout を設定(例: 10分)
- 長時間バッチ処理は定期的に COMMIT する
- pg_stat_activity で長時間 TX を監視
SET idle_in_transaction_session_timeout = '10min';
-- → 10分以上 idle in transaction 状態の接続を自動切断
まとめ
| 概念 | 役割 | 設定ポイント |
|---|---|---|
| VACUUM | Dead Tuple の空間を再利用可能にする | autovacuum_vacuum_scale_factor |
| VACUUM FULL | テーブルを物理的に縮小(ロックあり) | 本番では避ける → pg_repack |
| autovacuum | 自動 VACUUM の実行 | テーブルごとにカスタマイズ |
| Freeze | TX ID Wraparound 防止 | autovacuum_freeze_max_age |
| Bloat 監視 | pgstattuple / dead_tuple_percent | 20% 超で対策 |
| 長時間 TX 対策 | VACUUM のブロッカー排除 | idle_in_transaction_session_timeout |