PostgreSQL メンテナンスガイド
このドキュメントでは、PostgreSQLの定期メンテナンスとテーブル肥大化 対策を解説します。
目次
1. メンテナンスの概要
1.1 なぜメンテナンスが必要か
┌──────────────────────── ──────────────────────────────────────┐
│ メンテナンスが必要な理由 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【MVCCによる死んだ行の蓄積】 │
│ │
│ UPDATE文の動作: │
│ 1. 既存行を「削除済み」としてマーク (xmaxを設定) │
│ 2. 新しい行を挿入 │
│ → 古い行が「死んだ行 (dead tuple)」として残る │
│ │
│ DELETE文の動作: │
│ 1. 行を「削除済み」としてマーク │
│ → 実際には削除されず、死んだ行として残る │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ テーブル │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │Live │ │Dead │ │Live │ │Dead │ │Dead │ ... │ │
│ │ │Tuple│ │Tuple│ │Tuple│ │Tuple│ │Tuple│ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ └──────────────────────────────────────────────── ───────┘ │
│ │
│ 【問題】 │
│ - ディスク使用量の増加 │
│ - テーブルスキャンの効率低下 │
│ - インデックスの肥大化 │
│ - トランザクションIDの周回問題 │
│ │
│ 【解決策】 │
│ - VACUUM: 死んだ行の領域を再利用可能にする │
│ - ANALYZE: 統計情報を更新してクエリプランを最適化 │
│ - REINDEX: 肥大化したインデックスを再構築 │
│ │
└──────────────────────────────────────────────────────────────┘
1.2 メンテナンス作業の一覧
┌──────────────────────────────────────────────────────────────┐
│ メンテナンス作業一覧 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┬────────────────────────────────────────┐│
│ │ 作業 │ 目的 ││
│ ├────────────────┼────────────────────────────────────────┤│
│ │ VACUUM │ 死んだ行の領域を再利用可能にする ││
│ │ VACUUM FULL │ テーブルを完全に再構築 (排他ロック) ││
│ │ ANALYZE │ 統計情報を更新 ││
│ │ REINDEX │ インデックスを再構築 ││
│ │ CLUSTER │ インデックス順にテーブルを再配置 ││
│ │ pg_repack │ オンラインでテーブル/インデックス再構築││
│ └────────────────┴────────────────────────────────────────┘│
│ │
└──────────────────────────────────────────────────────────────┘
2. VACUUM
2.1 VACUUMの種類
┌──────────────────────────────────────────────────────────────┐
│ VACUUMの種類 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【VACUUM (通常)】 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ - 死んだ行を「再利用可能」としてマーク │ │
│ │ - ディスク容量はOSに返さない │ │
│ │ - テーブルをロックしない (同時に読み書き可能) │ │
│ │ - 比較的高速 │ │
│ │ - 日常的に使用 (Autovacuumで自動実行) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 【VACUUM FULL】 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ - テーブルを完全に再構築 │ │
│ │ - ディスク容量をOSに返す │ │
│ │ - 排他ロック (ACCESS EXCLUSIVE) が必要 │ │
│ │ - 作業中はアクセス不可 │ │
│ │ - 時間がかかる │ │
│ │ - 本当に必要な場合のみ使用 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 【VACUUM ANALYZE】 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ - VACUUM + ANALYZE を同時実行 │ │
│ │ - 日常的に推奨 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────── ──────────────────────────────────────────────────┘
2.2 VACUUMの実行
-- 全テーブルをVACUUM
VACUUM;
-- 特定 テーブルをVACUUM
VACUUM users;
-- VACUUM + ANALYZE
VACUUM ANALYZE users;
-- 詳細出力付き
VACUUM VERBOSE users;
-- VACUUM FULL (排他ロック、注意が必要)
VACUUM FULL users;
-- 並列VACUUM (PostgreSQL 13+)
VACUUM (PARALLEL 4) users;
2.3 VACUUMのオプション
-- PostgreSQL 12+ の構文
VACUUM (
FULL false, -- 通常VACUUM
FREEZE false, -- 強制的にFreeze
VERBOSE true, -- 詳細出力
ANALYZE true, -- 統計情報も更新
DISABLE_PAGE_SKIPPING false, -- 全ページをスキャン
SKIP_LOCKED false, -- ロックされたテーブルをスキップ
INDEX_CLEANUP auto, -- インデックスのクリーンアップ
PROCESS_TOAST true, -- TOASTテーブルも処理
TRUNCATE true, -- 末尾の空ページを切り詰め
PARALLEL 4 -- 並列ワーカー数
) users;
2.4 VACUUMの進捗確認
-- VACUUM の進捗状況
SELECT
p.pid,
p.datname,
p.relid::regclass AS table_name,
p.phase,
p.heap_blks_total,
p.heap_blks_scanned,
p.heap_blks_vacuumed,
ROUND(100.0 * p.heap_blks_vacuumed / NULLIF(p.heap_blks_total, 0), 2) AS progress_pct,
p.index_vacuum_count,
p.max_dead_tuples,
p.num_dead_tuples
FROM pg_stat_progress_vacuum p;
-- Autovacuumの実行状況
SELECT
pid,
datname,
relid::regclass AS table_name,
phase,
heap_blks_total,
heap_blks_scanned
FROM pg_stat_progress_vacuum
WHERE datname = current_database();
2.5 Freezeとトランザクション周回問題
┌──────────────────────────────────────────────────────────────┐
│ トランザクションID周回問題 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【問題】 │
│ - トランザクションID (XID) は32ビット (約42億) │
│ - 使い切ると周回する │
│ - 古いXIDと新しいXIDの区別がつかなくなる │
│ → データが「未来」のデータとして見えなくなる │
│ │
│ 【対策: Freeze処理】 │
│ - VACUUMが古いXIDを「FrozenXID」に置き換え │
│ - FrozenXIDは「全トランザクションから見える」特別なID │
│ │
│ 【モニタリング】 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SELECT datname, age(datfrozenxid) FROM pg_database; │ │
│ │ │ │
│ │ age() の値が大きいほど危険 │ │
│ │ - 10億未満: 正常 │ │
│ │ - 10億-15億: 注意 │ │
│ │ - 15億-20億: 警告、VACUUM FREEZEを検討 │ │
│ │ - 20億に近い: 緊急対応が必要 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
-- データベースのフリーズ年齢を確認
SELECT
datname,
age(datfrozenxid) AS age,
datfrozenxid
FROM pg_database
ORDER BY age DESC;
-- テーブルのフリーズ年齢を確認
SELECT
schemaname,
relname,
age(relfrozenxid) AS age,
relfrozenxid
FROM pg_stat_user_tables
ORDER BY age DESC
LIMIT 20;
-- 強制的にFreeze
VACUUM FREEZE users;
VACUUM (FREEZE) users;
3. ANALYZE
3.1 ANALYZEの役割
┌──────────────────────────────────────────────────────────────┐
│ ANALYZEの役割 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【目的】 │
│ テーブルの統計情報を収集し、 │
│ プランナーが最適な実行計画を立てられるようにする │
│ │
│ 【収集される情報】 │
│ - 行数 (reltuples) │
│ - ページ数 (relpages) │
│ - NULL値の割合 (null_frac) │
│ - ユニーク値の数 (n_distinct) │
│ - 最頻値 (most_common_vals) │
│ - ヒストグラム (histogram_bounds) │
│ - 相関 (correlation) │
│ │
│ 【いつ実行すべきか】 │
│ - 大量のINSERT/UPDATE/DELETE後 │
│ - テーブル作成後 │
│ - インデックス作成後 │
│ - クエリの実行計画が最適でない場合 │
│ │
└──────────────────────────────────────────────────────────────┘