PostgreSQL インデックス設計ガイド
このドキュメントでは、PostgreSQLにおけるインデックスの仕組みと 効果的な設計方法について解説します。
目次
- インデックスの基本概念
- B-treeインデックス
- GINインデックス
- GiSTインデックス
- その他のインデックス
- 複合インデックス
- 部分インデック ス
- カバリングインデックス
- インデックスの運用
- 設計のベストプラクティス
1. インデックスの基本概念
1.1 インデックスとは
┌─────────────────────────────────────────────────────────────────┐
│ インデックスの役割 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 【本の索引のようなもの】 │
│ │
│ 本で「PostgreSQL」という単語を探す場合: │
│ ❌ 全ページを順番に読む → 遅い │
│ ✅ 索引で「PostgreSQL → p.123」を見つける → 速い │
│ │
│ データベースでも同様: │
│ ❌ テーブル全体をスキャン(Seq Scan)→ 遅い │
│ ✅ インデックスで位置を特定(Index Scan)→ 速い │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Table Index (email) │ │
│ │ ┌────┬──────────────────┐ ┌────────────────────┐ │ │
│ │ │ id │ email │ │ alice@... → row 1 │ │ │
│ │ ├────┼──────────────────┤ │ bob@... → row 2 │ │ │
│ │ │ 1 │ alice@example... │ │ carol@... → row 3 │ │ │
│ │ │ 2 │ bob@example... │ │ ... │ │ │
│ │ │ 3 │ carol@example... │ └────────────────────┘ │ │
│ │ │ ...│ ... │ ↑ ソートされている │ │
│ │ └────┴──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 インデックスの種類
┌─────────────────────────────────────────────────────────────────┐
│ インデックスの種類 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┬──────────────────────────────────────────┐ │
│ │ 種類 │ 用途 │ │
│ ├──────────────┼──────────────────────────────────────────┤ │
│ │ B-tree │ 等価・範囲検索(デフォルト、最も一般的)│ │
│ │ Hash │ 等価検索のみ(PostgreSQL 10+で改善) │ │
│ │ GIN │ 配列、JSONB、全文検索 │ │
│ │ GiST │ 地理情報、範囲型、全文検索 │ │
│ │ SP-GiST │ 不均一な分布のデータ │ │
│ │ BRIN │ 大きなテーブル、時系列データ │ │
│ └──────────────┴──────────────────────────────────────────┘ │
│ │
│ 【選択の目安】 │
│ ・通常の検索 → B-tree │
│ ・配列/JSONB → GIN │
│ ・地理情報/範囲 → GiST │
│ ・時系列の大きなテーブル → BRIN │
│ │
└─────────────────────────────────────────────────────────────────┘
1.3 インデックスのトレードオフ
┌─────────────────────────────────────────────────────────────────┐
│ インデックスのトレードオフ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 【メリット】 │
│ ✅ SELECT が高速化 │
│ ✅ ORDER BY が高速化 │
│ ✅ JOIN が高速化 │
│ │
│ 【デメリット】 │
│ ❌ INSERT が遅くなる(インデックス更新が必要) │
│ ❌ UPDATE が遅くなる(インデックス対象カラムの場合) │
│ ❌ DELETE が遅くなる(インデックス更新が必要) │
│ ❌ ディスク容量を消費 │
│ │
│ 【判断基準】 │
│ ・読み取りが多い → インデックス有効 │
│ ・書き込みが多い → インデックス最小限 │
│ ・カーディナリティが高い → インデックス有効 │
│ ・カーディナリティが低い → インデックス効果薄い │
│ │
└─────────────────────────────────────────────────────────────────┘
2. B-treeインデックス
2.1 B-treeの仕組み
┌─────────────────────────────────────────────────────────────────┐
│ B-tree 構造 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ │
│ │ 50 | 100 │ ← ルートノード │
│ └───────┬───────┘ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 20 | 35 │ │ 70 | 85 │ │ 120| 150 │ ← 中間 │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ Leaf │ │ Leaf │ │ Leaf │ ← リーフ │
│ │ nodes │ │ nodes │ │ nodes │ │
│ └───────┘ └───────┘ └───────┘ │
│ │
│ 【特徴】 │
│ ・ソート順を維持 │
│ ・等価検索: O(log n) │
│ ・範囲検索: O(log n + k) ※k = 結果件数 │
│ ・ORDER BY にも使える │
│ │
└─────────────────────────────────────────────────────────────────┘
2.2 B-treeが有効な操作
-- B-treeインデックスの作成
CREATE INDEX idx_users_email ON users (email);
-- 等価検索 ✅
SELECT * FROM users WHERE email = 'alice@example.com';
-- 範囲検索 ✅
SELECT * FROM users WHERE created_at >= '2024-01-01';
SELECT * FROM users WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';
-- 前方一致 ✅
SELECT * FROM users WHERE email LIKE 'alice%';
-- ソート ✅
SELECT * FROM users ORDER BY email;
-- 中間・後方一致 ❌(インデックス使用不可)
SELECT * FROM users WHERE email LIKE '%alice%';
SELECT * FROM users WHERE email LIKE '%@example.com';
-- 関数適用 ❌(通常はインデックス使用不可)
SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';
-- → 式インデックスで対応可能
CREATE INDEX idx_users_email_lower ON users (LOWER(email));
2.3 NULLとB-tree
-- B-treeはNULLも格納できる
CREATE INDEX idx_users_deleted_at ON users (deleted_at);
-- IS NULL検索にも使える
SELECT * FROM users WHERE deleted_at IS NULL;
-- NULLの位置を制御
CREATE INDEX idx_users_deleted_at_nulls_first
ON users (deleted_at NULLS FIRST);
CREATE INDEX idx_users_deleted_at_nulls_last
ON users (deleted_at NULLS LAST);
2.4 内部構造と計算量の詳細
データ保存形式
PostgreSQL の B-tree は B+tree の派生で、データはデ ィスク上の 8KB page 単位で格納される。
┌──────────────────────────────────────────────────────────┐
│ ルートページ (8KB) │
│ ┌────────────────────────────────────────────────┐ │
│ │ Header │ Item₁ │ Item₂ │ Item₃ │ ... │ free │ │
│ └────────────────────────────────────────────────┘ │
│ 各 Item = (key値, 子ページへのポインタ) │
└──────────────────────────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 内部ページ│ │ 内部ページ│ │ 内部ページ│
└──────────┘ └──────────┘ └──────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ リーフページ │ │ リーフページ │ │ リーフページ │
│ (key + TID) │←→│ (key + TID) │←→│ (key + TID) │
└─────────────┘ └─────────────┘ └─────────────┘
↑ 葉同士は双方向リンクで連結(範囲スキャン高速化)
- エントリ単位: 1 行 = 1 エントリ。各エントリは
(キー値, TID = テーブル内タプル位置) - page サイズ: 8KB 固定。1 page に格納できるエントリ数は
8KB / (key サイズ + tid 6byte + メタ)- 例:
bigint(8 byte) のキーなら 1 page に約 400 エントリ - 例:
uuid(16 byte) のキーなら 1 page に約 250 エントリ
- 例:
- 高さの目安: ファンアウト ~250〜400 として、高さ
hで400^h行- 100 万行: 高さ 3
- 1 億行: 高さ 4
- 10 億行: 高さ 4〜5
- どんなに大きくても通常 4〜5 段で済むのが B-tree の強み