メインコンテンツまでスキップ

PostgreSQL プランナー深掘りガイド

プランナー(クエリオプティマイザ)は、SQLを「どのように実行するか」を決定するPostgreSQLの頭脳です。このドキュメントでは、プランナーの内部動作を詳しく解説します。


目次

  1. プランナーの役割と全体像
  2. 統計情報の仕組み
  3. コスト計算モデル
  4. スキャン方式の選択
  5. 結合アルゴリズムの選択
  6. 結合順序の最適化
  7. 実行計画の読み方
  8. プランナーの制御とチューニング
  9. プランナーが苦手なケース
  10. 実践的なトラブルシューティング

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を下げる等の調整可 │
│ │
└──────────────────────────────────────────────────────────────┘

3.2 EXPLAINで見るコスト

EXPLAIN SELECT * FROM users WHERE id = 100;
Index Scan using users_pkey on users  (cost=0.29..8.30 rows=1 width=100)
~~~~~ ~~~~~
│ │
startup cost total cost
┌──────────────────────────────────────────────────────────────┐
│ コストの読み方 │
├──────────────────────────────────────────────────────────────┤
│ │
│ cost=0.29..8.30 │
│ ~~~~ ~~~~ │
│ │ └─ total cost: 全行を取得するまでの総コスト │
│ │ │
│ └─ startup cost: 最初の行を返すまでのコスト │
│ (ソートや集約では大きくなる) │
│ │
│ rows=1 : 推定結果行数 │
│ width=100 : 1行あたりの推定バイト数 │
│ │
└──────────────────────────────────────────────────────────────┘

3.3 Sequential Scan のコスト計算

┌──────────────────────────────────────────────────────────────┐
│ Sequential Scan のコスト計算式 │
├──────────────────────────────────────────────────────────────┤
│ │
│ total_cost = seq_page_cost × ページ数 │
│ + cpu_tuple_cost × 行数 │
│ + cpu_operator_cost × 行数 × WHERE条件数 │
│ │
│ 【例】users テーブル: 10000行, 500ページ │
│ WHERE status = 'active' │
│ │
│ I/Oコスト = 1.0 × 500 = 500 │
│ CPUコスト = 0.01 × 10000 + 0.0025 × 10000 = 125 │
│ 総コスト = 625 │
│ │
└──────────────────────────────────────────────────────────────┘

3.4 Index Scan のコスト計算

┌──────────────────────────────────────────────────────────────┐
│ Index Scan のコスト計算式 │
├──────────────────────────────────────────────────────────────┤
│ │
│ total_cost = インデックス探索コスト │
│ + random_page_cost × 読み取りページ数 │
│ + cpu_index_tuple_cost × インデックス行数 │
│ + cpu_tuple_cost × テーブル行数 │
│ │
│ 【重要】correlation が考慮される │
│ │
│ correlation高い → ページ読み取りがシーケンシャルに近い │
│ → コストが下がる │
│ │
│ correlation低い → ランダムI/Oが多い │
│ → コストが上がる │
│ │
│ 【例】users.id にインデックス、id = 100 を検索 │
│ 推定1行、インデックス深さ3、correlation = 1.0 │
│ │
│ インデックス探索 = random_page_cost × 3 = 12 │
│ テーブル読み取り = random_page_cost × 1 = 4 │
│ CPU処理 = 約 0.03 │
│ 総コスト ≈ 16 │
│ │
└──────────────────────────────────────────────────────────────┘

3.5 コスト比較の例

-- 10000行のusersテーブル、statusカラムに5種類の値
-- 1. 選択性が高い場合 (1行のみ)
EXPLAIN SELECT * FROM users WHERE id = 100;
-- → Index Scan (cost ≈ 8)

-- 2. 選択性が中程度 (2000行 = 20%)
EXPLAIN SELECT * FROM users WHERE status = 'active';
-- → Index Scan または Bitmap Scan (cost比較で決定)

-- 3. 選択性が低い (8000行 = 80%)
EXPLAIN SELECT * FROM users WHERE status != 'deleted';
-- → Sequential Scan (cost ≈ 625)
-- インデックスを使うより全件スキャンの方が効率的

3.6 コストパラメータのチューニング

-- SSDを使用している場合(ランダム読み取りが速い)
SET random_page_cost = 1.1; -- デフォルト 4.0

-- メモリが潤沢でデータがキャッシュされている場合
SET effective_cache_size = '8GB'; -- OSキャッシュも含めた推定

-- 確認
SHOW random_page_cost;
SHOW seq_page_cost;

4. スキャン方式の選択

4.1 スキャン方式の一覧

┌──────────────────────────────────────────────────────────────┐
│ スキャン方式の種類 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ Sequential Scan │ テーブル全体を先頭から順に読む │
│ └─────────────────┘ │
│ ↓ 選択性が低い場合に有利 │
│ │
│ ┌─────────────────┐ │
│ │ Index Scan │ インデックス→テーブルの順でアクセス │
│ └─────────────────┘ │
│ ↓ 選択性が高い場合に有利 │
│ │
│ ┌─────────────────┐ │
│ │Index Only Scan │ インデックスだけで完結(テーブル不要) │
│ └─────────────────┘ │
│ ↓ SELECT対象がインデックスに含まれる場合 │
│ │
│ ┌─────────────────┐ │
│ │ Bitmap Scan │ 複数条件をビットマップで効率化 │
│ └─────────────────┘ │
│ ↓ 中程度の選択性、複数インデックス │
│ │
│ ┌─────────────────┐ │
│ │ TID Scan │ 直接行のTIDを指定してアクセス │
│ └─────────────────┘ │
│ ↓ WHERE ctid = '(0,1)' など特殊ケース │
│ │
└──────────────────────────────────────────────────────────────┘

4.2 Sequential Scan

┌──────────────────────────────────────────────────────────────┐
│ Sequential Scan │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【動作】 │
│ テーブルの全ページを先頭から順番に読み、各行をチェック │
│ │
│ Page 0 → Page 1 → Page 2 → ... → Page N │
│ ↓ ↓ ↓ ↓ │
│ 各行をWHERE条件でフィルタリング │
│ │
│ 【選ばれる条件】 │
│ - 取得行が全体の大部分を占める │
│ - 適切なインデックスがない │
│ - テーブルが小さい │
│ - correlation が低くIndex Scanが非効率 │
│ │
│ 【パラレル実行】 │
│ 大きなテーブルでは複数ワーカーで並列実行可能 │
│ → Parallel Sequential Scan │
│ │
└──────────────────────────────────────────────────────────────┘

4.3 Index Scan

┌──────────────────────────────────────────────────────────────┐
│ Index Scan │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【動作】 │
│ │
│ Step 1: インデックスを探索して条件に合うエントリを見つける │
│ │
│ B-tree Index │
│ │ │
│ ▼ │
│ [Root] → [Internal] → [Leaf] │
│ │ │
│ TID (0, 5) ← 行の物理位置 │
│ │
│ Step 2: TIDを使ってテーブルから行を取得 │
│ │
│ Table │
│ Page 0: [row1][row2][row3]... │
│ ↑ │
│ ここを読む │
│ │
│ 【選ばれる条件】 │
│ - 選択性が高い(少数の行を取得) │
│ - ORDER BYがインデックス順と一致 │
│ - LIMITと組み合わせ │
│ │
└──────────────────────────────────────────────────────────────┘

4.4 Index Only Scan

┌──────────────────────────────────────────────────────────────┐
│ Index Only Scan │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【動作】 │
│ インデックスだけで必要なデータが揃う場合、テーブルを読まない │
│ │
│ CREATE INDEX idx_users_email ON users(email); │
│ SELECT email FROM users WHERE email LIKE 'a%'; │
│ │
│ Index (email) │
│ ┌──────────────────────┐ │
│ │ alice@example.com │ ← これだけで回答可能 │
│ │ anna@example.com │ │
│ │ ... │ │
│ └──────────────────────┘ │
│ ↓ │
│ テーブルアクセス不要! │
│ │
│ 【条件】 │
│ 1. SELECTするカラムが全てインデックスに含まれる │
│ 2. Visibility Map で行が「全可視」と分かっている │
│ (VACUUMが適切に実行されている必要あり) │
│ │
│ 【カバリングインデックス】 │
│ CREATE INDEX idx_covering ON users(email) INCLUDE (name); │
│ → SELECT email, name FROM users WHERE email = '...' │
│ もIndex Only Scanにできる │
│ │
└──────────────────────────────────────────────────────────────┘

4.5 Bitmap Scan

┌──────────────────────────────────────────────────────────────┐
│ Bitmap Scan │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【動作】3段階のプロセス │
│ │
│ ① Bitmap Index Scan: インデックスから該当ページをビットマップ化│
│ │
│ Index Bitmap │
│ ┌────────┐ ┌───────────────────┐ │
│ │id = 5 │──→ │ Page 0: 1 │ │
│ │id = 12 │──→ │ Page 1: 0 │ │
│ │id = 15 │──→ │ Page 2: 1 │ │
│ │id = 23 │──→ │ Page 3: 1 │ │
│ └────────┘ └───────────────────┘ │
│ │
│ ② BitmapAnd / BitmapOr: 複数条件のビットマップを結合 │
│ │
│ WHERE status='active' AND category='A' │
│ │
│ Bitmap(status) Bitmap(category) Result │
│ [1,0,1,1,0] AND [1,1,0,1,0] = [1,0,0,1,0] │
│ │
│ ③ Bitmap Heap Scan: ビットマップに基づいてテーブルを読む │
│ │
│ Page 0, Page 3 のみ読み取り │
│ → ページ単位でまとめて読むのでランダムI/O削減 │
│ │
│ 【選ばれる条件】 │
│ - 中程度の選択性(多すぎず少なすぎず) │
│ - 複数の条件をOR/ANDで組み合わせる │
│ - correlation が低い(ランダムI/Oを減らしたい) │
│ │
└──────────────────────────────────────────────────────────────┘

4.6 スキャン方式の選択基準

┌──────────────────────────────────────────────────────────────┐
│ スキャン方式の選択フロー │
├──────────────────────────────────────────────────────────────┤
│ │
│ 適切なインデックスがある? │
│ │ │
│ No │ Yes │
│ ↓ └────────────────┐ │
│ Seq Scan │ │
│ 選択性は? │
│ │ │ │ │
│ 高い 中程度 低い │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Index Bitmap Seq │
│ Scan Scan Scan │
│ │ │
│ SELECT列がインデックスに │
│ 全て含まれる? │
│ │ │ │
│ Yes No │
│ │ │ │
│ ▼ ▼ │
│ Index Index │
│ Only Scan │
│ Scan │
│ │
└──────────────────────────────────────────────────────────────┘

【選択性の目安】
- 高い: 全体の 1-5% 未満 → Index Scan
- 中程度: 5-20% → Bitmap Scan
- 低い: 20% 以上 → Sequential Scan

※ テーブルサイズ、correlation、effective_cache_size等で変動

5. 結合アルゴリズムの選択

5.1 結合アルゴリズムの一覧

┌──────────────────────────────────────────────────────────────┐
│ 結合アルゴリズム │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Nested Loop Join │ │
│ │ 外側テーブルの各行に対し、内側テーブルを検索 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Hash Join │ │
│ │ 一方のテーブルでハッシュテーブルを構築し、 │ │
│ │ もう一方をそれに照合 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Merge Join │ │
│ │ 両テーブルをソートしてマージ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘

5.2 Nested Loop Join

┌──────────────────────────────────────────────────────────────┐
│ Nested Loop Join │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【アルゴリズム】 │
│ │
│ for each row in outer_table: # 外側ループ │
│ for each row in inner_table: # 内側ループ │
│ if join_condition matches: │
│ output combined row │
│ │
│ 【図解】 │
│ │
│ Outer (users) Inner (orders) │
│ ┌──────────┐ ┌──────────┐ │
│ │ id=1 │────┬────→│ user_id │ 全件または │
│ │ id=2 │────┼────→│ をスキャン│ インデックスで検索 │
│ │ id=3 │────┼────→│ │ │
│ │ ... │ │ └──────────┘ │
│ └──────────┘ │ │
│ └─ 外側の各行に対して内側を検索 │
│ │
│ 【コスト】 │
│ O(N × M) ... ただし内側にインデックスがあれば O(N × log M) │
│ │
│ 【適したケース】 │
│ - 外側テーブルが小さい │
│ - 内側テーブルにインデックスがある │
│ - 非等価結合(<, >, BETWEEN など) │
│ - LIMIT と組み合わせ(早期終了できる) │
│ │
└──────────────────────────────────────────────────────────────┘

5.3 Hash Join

┌──────────────────────────────────────────────────────────────┐
│ Hash Join │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【アルゴリズム】2フェーズ │
│ │
│ Phase 1: Build (ハッシュテーブル構築) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Inner Table (通常は小さい方) │ │
│ │ ┌────────────┐ │ │
│ │ │ id=1, ... │──→ hash(1) = bucket[3] │ │
│ │ │ id=2, ... │──→ hash(2) = bucket[7] │ │
│ │ │ id=3, ... │──→ hash(3) = bucket[3] │ │
│ │ └────────────┘ │ │
│ │ Hash Table │ │
│ │ ┌─────────────────┐ │ │
│ │ │ [0]: │ │ │
│ │ │ [1]: │ │ │
│ │ │ [2]: │ │ │
│ │ │ [3]: id=1, id=3 │ │ │
│ │ │ ... │ │ │
│ │ │ [7]: id=2 │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Phase 2: Probe (照合) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Outer Table の各行をハッシュテーブルで検索 │ │
│ │ │ │
│ │ user_id=1 → hash(1) → bucket[3] → マッチ! │ │
│ │ user_id=5 → hash(5) → bucket[2] → なし │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 【コスト】 │
│ O(N + M) ... ハッシュ構築O(M) + 照合O(N) │
│ │
│ 【適したケース】 │
│ - 等価結合 (=) のみ │
│ - 両テーブルが比較的大きい │
│ - work_mem に収まるサイズ │
│ - インデックスがない場合 │
│ │
│ 【注意】 │
│ - ハッシュテーブルがwork_memを超えると一時ファイルに退避 │
│ → 大幅に遅くなる │
│ │
└──────────────────────────────────────────────────────────────┘

5.4 Merge Join

┌──────────────────────────────────────────────────────────────┐
│ Merge Join │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【アルゴリズム】 │
│ │
│ 前提: 両テーブルが結合キーでソート済み │
│ │
│ Sorted Outer Sorted Inner │
│ ┌──────────┐ ┌──────────┐ │
│ │ id=1 │←──→│ user_id=1│ マッチ! │
│ │ id=2 │←──→│ user_id=2│ マッチ! │
│ │ id=3 │ │ user_id=2│ (2と複数マッチ) │
│ │ id=4 │←──→│ user_id=4│ マッチ! │
│ │ id=5 │ │ user_id=6│ (5はスキップ) │
│ │ id=6 │←──→│ user_id=6│ マッチ! │
│ └──────────┘ └──────────┘ │
│ ↓ ↓ │
│ 同時に進める(マージ) │
│ │
│ 【コスト】 │
│ O(N log N + M log M + N + M) │
│ ソート済みなら O(N + M) │
│ │
│ 【適したケース】 │
│ - 大きなテーブル同士の結合 │
│ - 既にソート済み(インデックスがある) │
│ - ORDER BY が結合キーと同じ │
│ - 等価結合だけでなく範囲結合も可能 │
│ │
└──────────────────────────────────────────────────────────────┘

5.5 結合アルゴリズムの選択基準

┌──────────────────────────────────────────────────────────────┐
│ 結合アルゴリズムの選択フロー │
├──────────────────────────────────────────────────────────────┤
│ │
│ 結合条件は? │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ 等価(=) 非等価(<,>等) │
│ │ │ │
│ │ └──→ Nested Loop (ほぼ唯一の選択肢) │
│ │ │
│ ▼ │
│ テーブルサイズは? │
│ │ │
│ ┌────┼────────────┐ │
│ │ │ │ │
│ 小×小 小×大 大×大 │
│ │ │ │ │
│ │ │ ┌──────┴──────┐ │
│ │ │ │ │ │
│ │ │ ソート済み? ソートなし │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ NL NL Merge Hash │
│(idx) (idx) Join Join │
│ │
└──────────────────────────────────────────────────────────────┘

【判断の補足】
- 小さいテーブル: 数千行以下
- 大きいテーブル: 数万行以上
- インデックスの有無で Nested Loop の効率が大きく変わる
- work_mem のサイズで Hash Join の効率が変わる

5.6 実行計画での確認例

EXPLAIN (ANALYZE, BUFFERS) 
SELECT u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active';
-- Nested Loop の例
Nested Loop (cost=0.29..125.35 rows=100 ...)
-> Index Scan using idx_users_status on users u (...)
Index Cond: (status = 'active')
-> Index Scan using idx_orders_user_id on orders o (...)
Index Cond: (user_id = u.id)

-- Hash Join の例
Hash Join (cost=15.00..85.00 rows=1000 ...)
Hash Cond: (o.user_id = u.id)
-> Seq Scan on orders o (...)
-> Hash (...)
-> Seq Scan on users u (...)
Filter: (status = 'active')

-- Merge Join の例
Merge Join (cost=200.00..350.00 rows=1000 ...)
Merge Cond: (u.id = o.user_id)
-> Index Scan using users_pkey on users u (...)
-> Sort (...)
Sort Key: o.user_id
-> Seq Scan on orders o (...)

6. 結合順序の最適化

6.1 結合順序問題

複数テーブルを結合する場合、順序によって性能が大きく変わります。

┌──────────────────────────────────────────────────────────────┐
│ 結合順序の重要性 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 3テーブル結合: A ⋈ B ⋈ C │
│ │
│ 可能な順序: │
│ 1. (A ⋈ B) ⋈ C │
│ 2. (A ⋈ C) ⋈ B │
│ 3. (B ⋈ C) ⋈ A │
│ 4. A ⋈ (B ⋈ C) │
│ 5. B ⋈ (A ⋈ C) │
│ 6. C ⋈ (A ⋈ B) │
│ │
│ 【例】 │
│ A: 10行, B: 10000行, C: 100行 │
│ A ⋈ B: 1000行, B ⋈ C: 5000行, A ⋈ C: 100行 │
│ │
│ 悪い順序: (B ⋈ C) ⋈ A │
│ → B ⋈ C で5000行の中間結果 │
│ → 5000行 ⋈ A │
│ │
│ 良い順序: (A ⋈ C) ⋈ B │
│ → A ⋈ C で100行の中間結果 │
│ → 100行 ⋈ B │
│ │
└──────────────────────────────────────────────────────────────┘

6.2 結合順序の探索方法

┌──────────────────────────────────────────────────────────────┐
│ 結合順序の探索アルゴリズム │
├──────────────────────────────────────────────────────────────┤
│ │
│ テーブル数が少ない場合 (≤ geqo_threshold, デフォルト12) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 動的計画法 (Dynamic Programming) │ │
│ │ │ │
│ │ 全ての組み合わせを評価して最適解を見つける │ │
│ │ 計算量: O(n! × 2^n) │ │
│ │ → 確実に最適解が得られる │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ テーブル数が多い場合 (> geqo_threshold) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GEQO (遺伝的アルゴリズム) │ │
│ │ │ │
│ │ ランダムな順序から始めて、進化的に改善 │ │
│ │ 計算量: 設定可能 │ │
│ │ → 最適解は保証されないが、現実的な時間で完了 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘

6.3 結合順序に関する設定

-- 動的計画法を使うテーブル数の閾値
SHOW geqo_threshold; -- デフォルト 12

-- GEQOの動作を制御
SET geqo = on; -- GEQO を有効化
SET geqo_effort = 5; -- 探索の努力度 (1-10)
SET geqo_generations = 0; -- 世代数 (0=自動)
SET geqo_pool_size = 0; -- 集団サイズ (0=自動)

-- 特定の結合順序を強制(デバッグ用)
SET join_collapse_limit = 1; -- 明示的な順序を維持

6.4 FROM句の書き方による制御

-- 通常: プランナーが自由に順序を決定
SELECT * FROM a, b, c WHERE a.id = b.a_id AND b.id = c.b_id;

-- CROSS JOIN LATERAL: 左から右への順序を強制
SELECT * FROM a
CROSS JOIN LATERAL (SELECT * FROM b WHERE b.a_id = a.id) b
CROSS JOIN LATERAL (SELECT * FROM c WHERE c.b_id = b.id) c;

-- 明示的な括弧で順序をヒント
SELECT * FROM a JOIN (b JOIN c ON b.id = c.b_id) ON a.id = b.a_id;
-- join_collapse_limit = 1 の場合、この順序が維持される

7. 実行計画の読み方

7.1 EXPLAINの種類

-- 基本的な実行計画
EXPLAIN SELECT ...;

-- 実際の実行時間も表示
EXPLAIN ANALYZE SELECT ...;

-- バッファ情報も表示
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;

-- 詳細情報
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT ...;

-- JSON形式で出力
EXPLAIN (ANALYZE, FORMAT JSON) SELECT ...;

7.2 実行計画の読み方の基本

┌──────────────────────────────────────────────────────────────┐
│ 実行計画の読み方 │
├──────────────────────────────────────────────────────────────┤
│ │
│ EXPLAIN ANALYZE の出力例: │
│ │
│ HashAggregate (cost=150..160 rows=100 width=8) │
│ (actual time=5.2..5.5 rows=95 loops=1) │
│ Group Key: status │
│ -> Hash Join (cost=25..140 rows=1000 width=4) │
│ (actual time=1.2..4.8 rows=980 loops=1) │
│ Hash Cond: (o.user_id = u.id) │
│ -> Seq Scan on orders o (cost=0..50 rows=2000) │
│ (actual time=0.01..1.5) │
│ -> Hash (cost=20..20 rows=500 width=4) │
│ (actual time=1.0..1.0 rows=490 loops=1) │
│ -> Seq Scan on users u (cost=0..20) │
│ (actual time=0.01..0.8)│
│ Filter: (status = 'active') │
│ Rows Removed by Filter: 510 │
│ Planning Time: 0.5 ms │
│ Execution Time: 6.0 ms │
│ │
│ 【読み方のポイント】 │
│ 1. 下から上に読む(実行順序) │
│ 2. インデントが深いほど先に実行される │
│ 3. cost の actual と estimated を比較 │
│ 4. rows の actual と estimated を比較 │
│ │
└──────────────────────────────────────────────────────────────┘

7.3 よく見るノードタイプ

┌──────────────────────────────────────────────────────────────┐
│ 主要なノードタイプ │
├──────────────────────────────────────────────────────────────┤
│ │
│ 【スキャン系】 │
│ Seq Scan : 全件スキャン │
│ Index Scan : インデックス使用 │
│ Index Only Scan : インデックスのみで完結 │
│ Bitmap Heap Scan : ビットマップスキャン │
│ │
│ 【結合系】 │
│ Nested Loop : ネストループ結合 │
│ Hash Join : ハッシュ結合 │
│ Merge Join : マージ結合 │
│ │
│ 【集約系】 │
│ Aggregate : 集約 (COUNT, SUM等) │
│ HashAggregate : ハッシュを使った GROUP BY │
│ GroupAggregate : ソートを使った GROUP BY │
│ │
│ 【ソート・制限】 │
│ Sort : ソート処理 │
│ Limit : 結果行数の制限 │
│ Unique : 重複排除 │
│ │
│ 【その他】 │
│ Materialize : 中間結果の具体化 │
│ Subquery Scan : サブクエリのスキャン │
│ CTE Scan : WITH句のスキャン │
│ Append : UNION ALL等の結合 │
│ │
└──────────────────────────────────────────────────────────────┘

7.4 推定値と実際の値のずれを見る

EXPLAIN ANALYZE SELECT * FROM users WHERE status = 'active';
┌──────────────────────────────────────────────────────────────┐
│ 推定 vs 実際 の比較 │
├──────────────────────────────────────────────────────────────┤
│ │
│ Seq Scan on users (cost=0.00..25.00 rows=500 width=100) │
│ (actual time=0.01..2.50 rows=2000 loops=1)│
│ ~~~~ ~~~~ │
│ 推定500行 実際2000行 │
│ │
│ 【問題】4倍のずれ │
│ │
│ 原因の可能性: │
│ - 統計情報が古い → ANALYZE を実行 │
│ - 複雑な条件で推定が難しい │
│ - 相関のあるカラムの組み合わせ │
│ │
│ 【影響】 │
│ - 行数の推定が大きくずれると、結合アルゴリズムや │
│ スキャン方式の選択を誤る可能性 │
│ - 中間結果のメモリ確保が不適切になる │
│ │
└──────────────────────────────────────────────────────────────┘

7.5 BUFFERS出力の読み方

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE id = 100;
Index Scan using users_pkey on users  
(cost=0.29..8.30 rows=1 width=100)
(actual time=0.05..0.06 rows=1 loops=1)
Index Cond: (id = 100)
Buffers: shared hit=3 ← ここに注目!
Planning Time: 0.1 ms
Execution Time: 0.1 ms
┌──────────────────────────────────────────────────────────────┐
│ BUFFERS の読み方 │
├──────────────────────────────────────────────────────────────┤
│ │
│ shared hit=3 : 共有バッファ内で見つかった (キャッシュ) │
│ shared read=5 : ディスクから読み込んだ │
│ shared dirtied=2 : ダーティにしたページ数 │
│ shared written=1 : 書き込んだページ数 │
│ │
│ local hit/read/dirtied/written : 一時テーブル用 │
│ temp read/written : 一時ファイル (work_mem超過時) │
│ │
│ 【パフォーマンス指標】 │
│ - hit が多い = キャッシュ効率が良い │
│ - read が多い = I/Oが多い (遅い可能性) │
│ - temp が出現 = work_mem が不足 │
│ │
└──────────────────────────────────────────────────────────────┘

8. プランナーの制御とチューニング

8.1 プランナー関連の主要パラメータ

-- コストモデル
SET seq_page_cost = 1.0; -- シーケンシャル読み取りコスト
SET random_page_cost = 4.0; -- ランダム読み取りコスト (SSDなら 1.1-1.5)
SET cpu_tuple_cost = 0.01; -- 行処理コスト
SET cpu_index_tuple_cost = 0.005; -- インデックスエントリ処理コスト
SET cpu_operator_cost = 0.0025; -- 演算子実行コスト

-- メモリ関連
SET effective_cache_size = '4GB'; -- OSキャッシュを含めた見積もり
SET work_mem = '64MB'; -- ソート・ハッシュ用メモリ

-- 並列クエリ
SET max_parallel_workers_per_gather = 2;
SET parallel_tuple_cost = 0.1;
SET parallel_setup_cost = 1000.0;

8.2 スキャン方式の制御

-- 特定のスキャン方式を無効化(デバッグ・検証用)
SET enable_seqscan = off; -- Sequential Scan を避ける
SET enable_indexscan = off; -- Index Scan を避ける
SET enable_indexonlyscan = off; -- Index Only Scan を避ける
SET enable_bitmapscan = off; -- Bitmap Scan を避ける

-- 実験例: Sequential Scan を避けた場合の計画を見る
SET enable_seqscan = off;
EXPLAIN SELECT * FROM users WHERE status = 'active';
SET enable_seqscan = on; -- 元に戻す

8.3 結合アルゴリズムの制御

-- 結合アルゴリズムを無効化(デバッグ・検証用)
SET enable_nestloop = off; -- Nested Loop を避ける
SET enable_hashjoin = off; -- Hash Join を避ける
SET enable_mergejoin = off; -- Merge Join を避ける

-- 実験例: Hash Join を強制
SET enable_nestloop = off;
SET enable_mergejoin = off;
EXPLAIN SELECT * FROM users u JOIN orders o ON u.id = o.user_id;

8.4 統計情報の調整

-- カラムの統計精度を上げる
ALTER TABLE users ALTER COLUMN status SET STATISTICS 500;
-- デフォルト: 100, 最大: 10000
-- 値が大きいほど詳細なヒストグラムが作成される

-- テーブル全体のデフォルト統計精度
SET default_statistics_target = 200;

-- 拡張統計(複数カラムの相関)
CREATE STATISTICS stats_name ON col1, col2 FROM table_name;

-- 統計情報を更新
ANALYZE users;

8.5 実践的なチューニング例

-- 例1: SSDを使用している環境
ALTER SYSTEM SET random_page_cost = 1.1;
SELECT pg_reload_conf();

-- 例2: メモリが潤沢な環境
ALTER SYSTEM SET effective_cache_size = '12GB';
ALTER SYSTEM SET work_mem = '256MB'; -- 注意: 接続数 × work_mem が必要
SELECT pg_reload_conf();

-- 例3: 大規模な分析クエリ用
SET work_mem = '1GB'; -- セッション単位で変更
EXPLAIN ANALYZE SELECT ... (複雑な集計クエリ);
RESET work_mem; -- 元に戻す

-- 例4: 並列クエリを活用
SET max_parallel_workers_per_gather = 4;
SET parallel_tuple_cost = 0.01; -- 並列処理のコストを下げる

9. プランナーが苦手なケース

9.1 統計情報の問題

┌──────────────────────────────────────────────────────────────┐
│ 統計情報が不正確なケース │
├──────────────────────────────────────────────────────────────┤
│ │
│ ① 大量のデータ変更後にANALYZEしていない │
│ → 古い統計情報で判断 │
│ 解決: ANALYZE テーブル名; │
│ │
│ ② データの偏りが激しい │
│ 例: status = 'active' が 99%, 他は 1% │
│ most_common_vals に含まれていれば正確 │
│ 解決: SET STATISTICS を増やす │
│ │
│ ③ 相関のあるカラム │
│ 例: city と zip_code は連動している │
│ 解決: CREATE STATISTICS で拡張統計を作成 │
│ │
│ ④ 関数の結果 │
│ 例: WHERE LOWER(email) = 'alice@example.com' │
│ → 関数適用後の分布がわからない │
│ 解決: 式インデックス + ANALYZE │
│ │
└──────────────────────────────────────────────────────────────┘

9.2 推定が難しい条件

-- パターン1: パラメータ化されたクエリ (Prepared Statement)
PREPARE user_query AS SELECT * FROM users WHERE status = $1;
EXECUTE user_query('active');
-- → $1 の値がわからないので「平均的な」選択性を使う
-- generic plan と custom plan の切り替えに注意

-- パターン2: 複雑な式
SELECT * FROM orders
WHERE total * 1.1 > 1000 AND EXTRACT(YEAR FROM created_at) = 2024;
-- → 式の評価後の分布は推定困難

-- パターン3: サブクエリの結果
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 1000);
-- → サブクエリの結果行数の推定が難しい

-- パターン4: OR条件
SELECT * FROM users
WHERE email = 'alice@example.com' OR phone = '090-1234-5678';
-- → 各条件の選択性を足し算するが、重複を過大評価しやすい

9.3 キャッシュ効果の見積もり

┌──────────────────────────────────────────────────────────────┐
│ キャッシュ効果の推定の難しさ │
├──────────────────────────────────────────────────────────────┤
│ │
│ プランナーは effective_cache_size を参考にするが... │
│ │
│ 【実際は予測困難】 │
│ - どのページがキャッシュに載っているか不明 │
│ - 同時に実行される他のクエリの影響 │
│ - OSのメモリ圧力 │
│ │
│ 【結果】 │
│ - 初回実行は遅く、2回目以降は速い(ウォームアップ効果) │
│ - EXPLAIN ANALYZE の結果と実運用で差が出ることがある │
│ │
│ 【対策】 │
│ - pg_prewarm で事前にキャッシュに載せる │
│ - effective_cache_size を適切に設定 │
│ │
└──────────────────────────────────────────────────────────────┘

9.4 多テーブル結合の計画時間

-- 多数のテーブルを結合すると計画時間が長くなる
SELECT * FROM t1
JOIN t2 ON t1.id = t2.t1_id
JOIN t3 ON t2.id = t3.t2_id
-- ... 15テーブル以上
;

-- 計画時間 >> 実行時間 になることも

-- 対策
SET geqo_threshold = 8; -- より早くGEQOに切り替え
SET from_collapse_limit = 8; -- サブクエリの平坦化を制限
SET join_collapse_limit = 8; -- 結合順序の探索を制限

10. 実践的なトラブルシューティング

10.1 遅いクエリの分析手順

┌──────────────────────────────────────────────────────────────┐
│ 遅いクエリの分析フロー │
├──────────────────────────────────────────────────────────────┤
│ │
│ Step 1: EXPLAIN ANALYZE で実行計画を取得 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ EXPLAIN (ANALYZE, BUFFERS, VERBOSE) │ │
│ │ SELECT ...; │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 2: 推定 vs 実際 の乖離を確認 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ rows=100 (推定) vs rows=50000 (実際) │ │
│ │ → 統計情報が古いか、推定が難しい条件 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 3: 最も時間がかかっているノードを特定 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ actual time=0.01..5000.00 ← ここが遅い │ │
│ │ → このノードの改善を検討 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 4: 改善策を実行 │
│ - ANALYZE でstatistics更新 │
│ - インデックスの追加/変更 │
│ - クエリの書き換え │
│ - パラメータのチューニング │
│ │
└──────────────────────────────────────────────────────────────┘

10.2 よくある問題と解決策

┌──────────────────────────────────────────────────────────────┐
│ 問題1: 不適切なスキャン方式 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 症状: インデックスがあるのに Seq Scan が選ばれる │
│ │
│ 原因と対策: │
│ 1. 選択性が低い → インデックスより Seq Scan が効率的 │
│ → 意図通りならOK │
│ │
│ 2. 統計情報が古い │
│ → ANALYZE テーブル名; │
│ │
│ 3. データ型の不一致 │
│ WHERE int_column = '100' (文字列) │
│ → 暗黙の型変換でインデックスが使えない │
│ → WHERE int_column = 100 (数値) │
│ │
│ 4. 関数適用 │
│ WHERE UPPER(name) = 'ALICE' │
│ → 式インデックスを作成 │
│ CREATE INDEX idx ON users (UPPER(name)); │
│ │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ 問題2: 不適切な結合順序 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 症状: 中間結果が巨大になり、メモリ不足やスワップ │
│ │
│ 確認方法: │
│ EXPLAIN で各結合ステップの rows を確認 │
│ → 途中で行数が爆発していないか │
│ │
│ 対策: │
│ 1. 統計情報の更新 (ANALYZE) │
│ 2. 結合条件の追加(フィルタを早める) │
│ 3. サブクエリや CTE で順序を制御 │
│ 4. join_collapse_limit を調整 │
│ │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ 問題3: ソートやハッシュでのディスク使用 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 症状: BUFFERS に temp read/written が出現 │
│ │
│ 原因: work_mem を超えたため一時ファイルを使用 │
│ │
│ 対策: │
│ 1. work_mem を増やす │
│ SET work_mem = '256MB'; │
│ │
│ 2. クエリを分割して中間結果を減らす │
│ │
│ 3. インデックスを活用してソートを回避 │
│ ORDER BY に合わせたインデックスを作成 │
│ │
└──────────────────────────────────────────────────────────────┘

10.3 実践的な EXPLAIN 分析例

-- 問題のあるクエリ
EXPLAIN (ANALYZE, BUFFERS)
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id, u.name
ORDER BY order_count DESC
LIMIT 10;
Limit  (cost=5000..5000 rows=10 width=40) 
(actual time=2500..2500 rows=10 loops=1)
Buffers: shared hit=100 read=5000, temp read=500 written=500
~~~~~~~~~~~~~~~~~~~~~~~~~~~
一時ファイル使用!
-> Sort (cost=4900..5000 rows=50000 width=40)
(actual time=2400..2400 rows=10 loops=1)
Sort Key: (count(o.id)) DESC
Sort Method: external merge Disk: 4000kB
~~~~~~~~~~~~~~ ~~~~~~~~~~~
ディスクソート!メモリ不足
-> HashAggregate (cost=3000..4000 rows=50000 width=40)
(actual time=1800..2200 rows=50000 loops=1)
-> Hash Left Join (cost=1000..2500 rows=100000 width=36)
(actual time=500..1500 rows=100000 loops=1)
-> Seq Scan on users u (cost=0..500 rows=50000)
(actual time=0..200 rows=50000)
Filter: (created_at > '2024-01-01')
-> Hash (cost=800..800 rows=100000)
(actual time=400..400 rows=100000 loops=1)
-> Seq Scan on orders o
Planning Time: 1.0 ms
Execution Time: 2550 ms
┌──────────────────────────────────────────────────────────────┐
│ 分析結果 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 問題点: │
│ 1. ディスクソートが発生 (Sort Method: external merge) │
│ 2. 50000行を集約した後にソート → 非効率 │
│ 3. users.created_at にインデックスがない可能性 │
│ │
│ 改善策: │
│ 1. work_mem を増やす │
│ SET work_mem = '128MB'; │
│ │
│ 2. インデックスを作成 │
│ CREATE INDEX idx_users_created_at ON users(created_at); │
│ │
│ 3. LIMIT を活用した書き換え(トップNの最適化) │
│ - 集約前にフィルタリングを強化 │
│ │
└──────────────────────────────────────────────────────────────┘

まとめ

プランナーを理解することで、以下のことができるようになります:

スキル活用場面
実行計画の読解クエリのボトルネック特定
統計情報の管理正確なコスト推定の維持
インデックス設計適切なスキャン方式の選択
パラメータチューニング環境に合わせた最適化
クエリ書き換えプランナーの苦手ケースの回避

参考リンク