PostgreSQL プランナー深掘りガイド
プランナー(クエリオプティマイザ)は、SQLを「どのように 実行するか」を決定するPostgreSQLの頭脳です。このドキュメントでは、プランナーの内部動作を詳しく解説します。
目次
- プランナーの役割と全体像
- 統計情報の仕組み
- コスト計算モデル
- スキャン方式の選択
- 結合アルゴリズムの選択
- 結合順序の最適化
- 実行計画の読み方
- プランナーの制御とチューニング
- プランナーが苦手なケース
- 実践的なトラブルシューティング
1. プランナーの役割 と全体像
1.1 プランナーとは
プランナーは、クエリ木(Query Tree)を受け取り、最適な実行計画(Plan Tree)を生成するコンポーネントです。
┌──────────────────────────────────────────────────────────────┐
│ プランナーの位置づけ │
├──────────────────────────────────────────────────────────────┤
│ │
│ SQL文 │
│ │ │
│ ▼ │
│ Parser (構文解析) │
│ │ │
│ ▼ │
│ Analyzer (意味解析) │
│ │ │
│ ▼ │
│ Rewriter (書き換え) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Planner │ │
│ │ │ │
│ │ 入力: Query Tree (何を取得するか) │ │
│ │ 出力: Plan Tree (どうやって取得するか) │ │
│ │ │ │
│ │ 「同じ結果を得る複数の方法」から最適なものを選ぶ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Executor (実行) │
│ │
└──────────────────────────────────────────────────────────────┘
1.2 プランナーの処理フロー
┌──────────────────────────────────────────────────────────────┐
│ プランナー内部の処理フロー │
├────────── ────────────────────────────────────────────────────┤
│ │
│ Query Tree │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ 1. 前処理 (Preprocessing) │ │
│ │ - サブクエリの平坦化 │ │
│ │ - 定数の畳み込み │ │
│ │ - WHERE句の正規化 │ │
│ └────────────────────┬───────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ 2. スキャンパスの生成 │ │
│ │ - 各テーブルのアクセス方法を列挙 │ │
│ │ - Sequential Scan, Index Scan等 │ │
│ └────────────────────┬───────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ 3. 結合パスの生成 │ │
│ │ - テーブルの結合方法を列挙 │ │
│ │ - Nested Loop, Hash Join等 │ │
│ └────────────────────┬───────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ 4. 最適パスの選択 │ │
│ │ - コストが最小のパスを選択 │ │
│ └────────────────────┬───────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ 5. Plan Tree の生成 │ │
│ │ - 選択したパスを実行計画に変換 │ │
│ └─── ─────────────────┬───────────────────┘ │
│ ▼ │
│ Plan Tree │
│ │
└──────────────────────────────────────────────────────────────┘
1.3 「パス」と「プラン」
プランナー内部では、まず複数の「パス (Path)」を生成し、最終的に最適なパスを「プラン (Plan)」に変換します。
【パス (Path)】
- 実行方法の候補
- コスト見積もりを持つ
- 複数のパスが並行して検討される
【プラン (Plan)】
- 最終的に採用された実行計画
- Executorが実際に実行する
2. 統計情報の仕組み
プランナーが適切な判断をするためには、テーブルの統計情報が不可欠です。
2.1 統計情報とは
┌──────────────────────────────────────────────────────────────┐
│ 統計情報の種類 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【テーブルレベル】pg_class │
│ ├─ reltuples: 推定行数 │
│ ├─ relpages: ページ数(8KBブロック数) │
│ └─ relallvisible: 全可視ページ数 │
│ │
│ 【カラムレベル】pg_statistic / pg_stats │
│ ├─ null_frac: NULL値の割合 │
│ ├─ n_distinct: ユニーク値の数(または割合) │
│ ├─ most_common_vals: 最頻値リスト │
│ ├─ most_common_freqs: 最頻値の出現頻度 │
│ ├─ histogram_bounds: ヒストグラム境界値 │
│ └─ correlation: 物理順序との相関 │
│ │
└──────────────────────── ──────────────────────────────────────┘
2.2 統計情報の確認方法
-- テーブルの基本統計
SELECT
relname,
reltuples::bigint AS estimated_rows,
relpages AS pages
FROM pg_class
WHERE relname = 'users';
-- カラムの詳細統計
SELECT
attname,
null_frac,
n_distinct,
most_common_vals,
most_common_freqs,
histogram_bounds
FROM pg_stats
WHERE tablename = 'users' AND attname = 'status';
2.3 n_distinct の解釈
┌──────────────────────────────────────────────────────────────┐
│ n_distinct の意味 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 正の値: ユニーク値の絶対数 │
│ 例: n_distinct = 5 → 5種類の値がある │
│ │
│ 負の値: 行数に対する割合(絶対値) │
│ 例: n_distinct = -0.5 → ユニーク値は行数の50% │
│ 10000行なら約5000種類 │
│ │
│ -1: すべての値がユニーク(主キーなど) │
│ │
└──────────────────────────────────────────────────────────────┘
2.4 ヒストグラム
ヒストグラムは値の分布を表し、範囲検索の選択性を推定するのに使われます。
┌──────────────────────────────────────────────────────────────┐
│ ヒストグラムの例 │
├──────────────────────────────────────────────────────────────┤
│ │
│ histogram_bounds = {0, 20, 40, 60, 80, 100} │
│ │
│ これは値を5つの等頻度バケットに分割: │
│ [0-20), [20-40), [40-60), [60-80), [80-100] │
│ 各バケットには約20%のデータが含まれる │
│ │
│ WHERE age BETWEEN 30 AND 50 の選択性推定: │
│ - [20-40) の一部 + [40-60) の一部 │
│ - 約 (10/20 × 20%) + (10/20 × 20%) = 20% │
│ │
│ 頻度 │
│ ▲ │
│ │ ████ ████ ████ ████ ████ │
│ │ ████ ████ ████ ████ ████ │
│ └──────────────────────────────────▶ 値 │
│ 0 20 40 60 80 100 │
│ │
└──────────────────────────────────────────────────────────────┘
2.5 相関 (correlation)
物理的な行の並び順と、カラム値の論理的な順序の相関を示します。
┌──────────────────────────────────────────────────────────────┐
│ correlation の意味 │
├──────────────────────────────────────────────────────────────┤
│ │
│ correlation = 1.0: 完全に順序通り │
│ 物理順: 行1, 行2, 行3, 行4, 行5 │
│ 値: 10, 20, 30, 40, 50 │
│ → Index Scan が効率的(連続読み取り) │
│ │
│ correlation = 0.0: ランダム │
│ 物理順: 行1, 行2, 行3, 行4, 行5 │
│ 値: 30, 10, 50, 20, 40 │
│ → Index Scan はランダムI/Oが多発 │
│ │
│ correlation = -1.0: 完全に逆順 │
│ 物理順: 行1, 行2, 行3, 行4, 行5 │
│ 値: 50, 40, 30, 20, 10 │
│ │
│ 【Index Scan のコストに影響】 │
│ correlation が低い → ランダムI/O → コスト高 │
│ correlation が高い → シーケンシャルI/O → コスト低 │
│ │
└── ────────────────────────────────────────────────────────────┘
2.6 統計情報の更新
-- 特定テーブルの統計情報を更新
ANALYZE users;
-- 全テーブルの統計情報を更新
ANALYZE;
-- 統計情報の精度を上げる(サンプル数を増やす)
ALTER TABLE users ALTER COLUMN status SET STATISTICS 1000;
ANALYZE users;
-- デフォルトは 100、最大 10000
2.7 拡張統計情報
PostgreSQL 10以降では、複数カラム間の相関を記録できます。
-- 複数カラムの相関統計を作成
CREATE STATISTICS stats_city_zip ON city, zip_code FROM addresses;
ANALYZE addresses;
-- 確認
SELECT * FROM pg_statistic_ext WHERE stxname = 'stats_city_zip';
┌──────────────────────────────────────────────────────────────┐
│ 拡張統計が必要なケース │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【問題】カラム間に相関があるのにプランナーが認識できない │
│ │
│ 例: addresses テーブル │
│ city = '東京都' AND zip_code LIKE '1%' │
│ │
│ 通常の統計: │
│ city = '東京都' の選択性: 10% │
│ zip_code LIKE '1%' の選択性: 10% │
│ 推定選択性: 10% × 10% = 1% ← 独立と仮定 │
│ │
│ 実際: │
│ 東京都の郵便番号は1で始まる → 実際は10% │
│ │
│ 拡張統計を使うと正確に推定できる │
│ │
└──────────────────────────────────────────────────────────────┘
3. コスト計算モデル
3.1 コストとは
プランナーの「コスト」は、実行にかかるリソースの相対的な見積もりです。
┌──── ──────────────────────────────────────────────────────────┐
│ コストの構成要素 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 総コスト = I/Oコスト + CPUコスト │
│ │
│ 【I/Oコスト】 │
│ ├─ シーケンシャル読み取り (seq_page_cost = 1.0) │
│ └─ ランダム読み取り (random_page_cost = 4.0) │
│ │
│ 【CPUコスト】 │
│ ├─ 行の処理 (cpu_tuple_cost = 0.01) │
│ ├─ インデックスエントリの処理 (cpu_index_tuple_cost = 0.005)│
│ └─ 演算子/関数の実行 (cpu_operator_cost = 0.0025) │
│ │
│ ※ 値はデフォルト。SSDならrandom_page_costを下げる等の調整可 │
│ │
└──────────────────────────────────────────────────────────────┘