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);
3. GINインデックス
3.1 GINの仕組み
┌─────────────────────────────────────────────────────────────────┐
│ GIN (Generalized Inverted Index) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 【転置インデックス】 │
│ │
│ 元データ: │
│ ┌─────┬─────────────────────────────┐ │
│ │ id │ tags │ │
│ ├─────┼─────────────────────────────┤ │
│ │ 1 │ ['java', 'spring', 'api'] │ │
│ │ 2 │ ['python', 'django', 'api']│ │
│ │ 3 │ ['java', 'android'] │ │
│ └─────┴─────────────────────────────┘ │
│ │
│ GINインデックス: │
│ ┌──────────────┬─────────────────┐ │
│ │ キー │ 行リスト │ │
│ ├──────────────┼─────────────────┤ │
│ │ 'android' │ [3] │ │
│ │ 'api' │ [1, 2] │ │
│ │ 'django' │ [2] │ │
│ │ 'java' │ [1, 3] │ │
│ │ 'python' │ [2] │ │
│ │ 'spring' │ [1] │ │
│ └──────────────┴─────────────────┘ │
│ │
│ → 'java'を含む記事を高速に検索可能 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 GINの用途
-- 配列検索
CREATE TABLE articles (
id BIGINT PRIMARY KEY,
title TEXT,
tags TEXT[]
);
CREATE INDEX idx_articles_tags ON articles USING gin (tags);
-- 配列に特定の値を含む
SELECT * FROM articles WHERE tags @> ARRAY['java'];
-- 配列のいずれかを含む
SELECT * FROM articles WHERE tags && ARRAY['java', 'python'];
-- JSONB検索
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name TEXT,
attributes JSONB
);
CREATE INDEX idx_products_attributes ON products USING gin (attributes);
-- JSONBに特定のキーと値を含む
SELECT * FROM products
WHERE attributes @> '{"color": "red"}';
-- JSONBの特定のキーが存在
SELECT * FROM products
WHERE attributes ? 'size';
-- 全文検索
CREATE INDEX idx_articles_body_fts
ON articles USING gin (to_tsvector('english', body));
SELECT * FROM articles
WHERE to_tsvector('english', body) @@ to_tsquery('english', 'postgresql & index');
3.3 GINのオプション
-- fastupdate(デフォルト: on)
-- 更新を遅延させてバッチ処理することで書き込み性能向上
CREATE INDEX idx_articles_tags ON articles
USING gin (tags) WITH (fastupdate = on);
-- gin_pending_list_limit
-- 遅延リストの最大サイズ(デフォルト: 4MB)
CREATE INDEX idx_articles_tags ON articles
USING gin (tags) WITH (gin_pending_list_limit = 8192); -- 8MB