Skip to content

✍️ 필사 모드: DBインデックス完全ガイド 2025: B-Tree, Hash, GIN, BRIN, 複合インデックス, 実行計画解析

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

TL;DR

  • B-Treeが90%: 多くのクエリに適合。ソート、範囲、完全一致を全て支援。
  • GINは検索最強: 配列、JSON、全文検索。PostgreSQLの秘密兵器。
  • 複合インデックスの順序が決定的: WHERE a=? AND b=?WHERE b=? AND a=? で使用可否が変わる。
  • Covering Indexでテーブルアクセスを回避: INCLUDE句を活用。
  • インデックスが使われない10の理由を把握 — 統計、関数、キャスト、パターンなど。

1. なぜインデックスは速いのか

1.1 インデックスなし

SELECT * FROM users WHERE email = 'alice@example.com';

インデックスなしだと Sequential Scan — 全行を読み比較。1億行なら1億回比較。

1億行 x 100 nanosecond = 10

1.2 B-Treeあり

1億行 -> log2(10^8) ~= 27回比較
27 x 100 ns = 2.7 microsecond

約400万倍速い — これがインデックスの魔法です。

1.3 インデックスのコスト

無料ではない:

  • ストレージ: ディスク容量を消費
  • 書き込み遅延: INSERT/UPDATE/DELETE時にインデックスも更新
  • 保守: VACUUM, REINDEX

ルール: 読み込みと書き込みの比率を考慮。100:1ならインデックス追加、1:100なら慎重に。


2. B-Tree内部構造

2.1 平衡木

                [50]
               /    \
          [25]      [75]
         /    \    /    \
      [10]  [40] [60]  [90]

特性:

  • 全リーフが同じ深さ(平衡)
  • 各ノードに数百キーを格納
  • ソート順を維持

2.2 ページ単位のストレージ

PostgreSQL B-Tree page (8KB)
+----------------------------------+
| Header                           |
+----------------------------------+
| Item Pointer 1 -> key1 + ctid    |
| Item Pointer 2 -> key2 + ctid    |
+----------------------------------+
| Free Space                       |
+----------------------------------+
| Items (low to high)              |
+----------------------------------+

ctid はPostgreSQLの行位置(ページ番号、オフセット)。

2.3 木の深さ

8KBページに200キーを想定:

  • Depth 1: 200 keys
  • Depth 2: 40,000
  • Depth 3: 8,000,000
  • Depth 4: 1,600,000,000

1億行 = 深さ4 -> ディスク4回読み込みで検索完了。


3. インデックスの種類 (PostgreSQL)

3.1 B-Tree (デフォルト)

CREATE INDEX idx_users_email ON users(email);

適合: 完全一致、範囲、ORDER BY、前方一致 LIKE 'abc%'IS NULL

不適合: LIKE '%abc'LIKE '%abc%'LOWER(email) のような関数適用(Expression Indexが必要)。

3.2 Hash Index

CREATE INDEX idx_users_email_hash ON users USING hash(email);

完全一致のみ。大抵の場合B-Treeが優位。PG 10+でWAL対応してから時々有用(わずかに小さい)。

3.3 GIN (Generalized Inverted Index)

-- 配列
CREATE INDEX idx_tags ON posts USING gin(tags);

-- JSONB
CREATE INDEX idx_metadata ON products USING gin(metadata);

-- 全文検索
CREATE INDEX idx_content_fts ON articles USING gin(to_tsvector('english', content));

転置インデックス — 各キーが行リストにマッピング。

"redis"    -> [row1, row5, row100, ...]
"kafka"    -> [row3, row7, row50, ...]
"postgres" -> [row1, row2, row100, ...]

適合: 配列包含 (tags @> ARRAY['redis'])、JSONBキー/値、全文検索、多値カラム。

SELECT * FROM posts WHERE tags @> ARRAY['redis'];
SELECT * FROM posts WHERE tags @> ARRAY['redis', 'database'];
SELECT * FROM products WHERE metadata @> '{"category": "electronics"}';

欠点: ビルドが遅い。fastupdate=on でUPDATEコストを遅延化。

3.4 GiST (Generalized Search Tree)

CREATE INDEX idx_locations ON places USING gist(location);
CREATE INDEX idx_periods ON events USING gist(period);

地理データ、範囲型、最近傍探索、重なり判定。

3.5 BRIN (Block Range Index)

CREATE INDEX idx_logs_created ON logs USING brin(created_at);

ページ範囲ごとにmin/maxのみ格納。非常に小さい。時系列やログのように物理順が揃った巨大テーブルに最適。1億行のログでBRINは数MB、B-Treeは数GB。

3.6 SP-GiST

CREATE INDEX idx_ips ON connections USING spgist(ip_addr);

非平衡木。IP範囲や一部のPostGIS型。

3.7 比較表

種類完全一致範囲パターン配列/JSON地理サイズビルド
B-Tree前方のみ不可不可
Hash不可不可不可不可
GIN不可全文不可
GiST不可不可
BRIN不可可*不可不可不可極小
SP-GiST不可不可

*BRINはデータが物理的にソートされている時のみ効果。


4. 複合インデックス

4.1 カラム順が決定的

CREATE INDEX idx_users_country_age ON users(country, age);

使用可能:

WHERE country = 'KR'
WHERE country = 'KR' AND age = 30
WHERE country = 'KR' AND age > 25

使用不可:

WHERE age = 30    -- countryが先頭
WHERE age > 25

ルール: leftmost prefix rule(左端プレフィックス)。

4.2 カラム順の決め方

優先順位:

  1. 頻繁に使われるカラムを左に
  2. 選択度の高いカラムを左に(多く絞り込む)
  3. 等号条件 > 範囲条件

例:

WHERE country = 'KR' AND age > 25 AND created_at > '2024-01-01'

選択度: country=30%、created_at=50%、age=70%。最適:

CREATE INDEX idx_users_search ON users(country, created_at, age);

4.3 等号+範囲

WHERE country = 'KR' AND age BETWEEN 25 AND 35

最適: (country, age) — 等号カラムを先に。


5. 特殊インデックスパターン

5.1 Partial Index

CREATE INDEX idx_active_users ON users(email)
WHERE status = 'active';

利点: インデックスサイズ縮小、高速ビルド/保守、キャッシュ効率向上。

5.2 Expression Index

CREATE INDEX idx_users_email_lower ON users(LOWER(email));

SELECT * FROM users WHERE LOWER(email) = LOWER('Alice@example.com');

一般的: LOWERUPPEREXTRACT(year FROM ...)、JSON抽出 (metadata->>'category')

5.3 Covering Index (INCLUDE)

-- PostgreSQL 11+
CREATE INDEX idx_users_country_email ON users(country) INCLUDE (email, name);

インデックスのみで結果を返す(Index Only Scan)。

SELECT email, name FROM users WHERE country = 'KR';

5.4 Unique Index

CREATE UNIQUE INDEX idx_users_email_unique ON users(email);

重複防止 + オプティマイザが一意性を活用。


6. EXPLAIN ANALYZE をマスター

6.1 基本

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM users WHERE country = 'KR' AND age > 25;

6.2 出力の読み方

Index Scan using idx_users_country_age on users
  (cost=0.43..2547.39 rows=10000 width=120)
  (actual time=0.025..15.234 rows=9847 loops=1)
  Index Cond: ((country)::text = 'KR'::text)
  Filter: (age > 25)
  Rows Removed by Filter: 153
  Buffers: shared hit=1234 read=56
Planning Time: 0.123 ms
Execution Time: 15.567 ms
  • cost: オプティマイザ推定
  • rows vs actual rows: 推定と実測
  • Buffers: キャッシュhit/read

6.3 推定 vs 実測

1000倍の乖離は統計不正確の証。ANALYZEを実行し default_statistics_target を上げる。

6.4 主要ノード

ノード意味良い?
Seq Scan全表スキャン小テーブルのみ
Index Scanインデックス+ヒープ参照
Index Only Scanインデックスのみ最良
Bitmap Index Scanビットマップ合成
Nested Loop小ジョイン小さい時のみ
Hash Join大ジョイン
Merge Joinソート済み結合
Sortメモリ/ディスクDiskに落ちたら注意

6.5 赤信号

  • Rows Removed by Filter: 100000+
  • Disk: external merge
  • estimated/actual > 100
  • 大テーブルに Seq Scan

7. インデックスが使われない10の理由

7.1 関数/式の適用

-- 悪い
SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';
-- 良い
CREATE INDEX idx_email_lower ON users(LOWER(email));

7.2 型キャスト

-- 悪い: varchar列に整数
SELECT * FROM users WHERE phone = 12345678;
-- 良い
SELECT * FROM users WHERE phone = '12345678';

7.3 先頭ワイルドカード

-- 悪い
SELECT * FROM users WHERE name LIKE '%alice%';
-- 良い
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_users_name_trgm ON users USING gin(name gin_trgm_ops);

7.4 NULL比較

CREATE INDEX idx_active ON users(id) WHERE deleted_at IS NULL;

7.5 OR条件

OR より INUNION を優先。

7.6 統計不正確

ANALYZE users;

7.7 戻り行数が多すぎる

約10%超を返すとプランナーはSeq Scanを選択しがち。Partial Indexや別戦略を検討。

7.8 オプティマイザヒント不在

PostgreSQLは意図的に非対応。MySQLは USE INDEX が可能。pg_hint_plan で追加可。

7.9 先頭カラム欠落

CREATE INDEX idx_users_country_age ON users(country, age);
-- 悪い: country欠落
SELECT * FROM users WHERE age > 25;

7.10 暗黙変換

-- 一部DBで悪い
SELECT * FROM orders WHERE order_id = 12345;
-- 良い
SELECT * FROM orders WHERE order_id = 12345::bigint;

8. 実践チューニング事例

8.1 遅い検索

SELECT * FROM products
WHERE category = 'electronics'
  AND price BETWEEN 100 AND 500
ORDER BY created_at DESC
LIMIT 20;

Before: Seq Scan + Sort (10秒)。修正:

CREATE INDEX idx_products_search ON products(category, price, created_at DESC);

After: Index Scan、5ms — 2000倍速い。

8.2 遅いJOIN

SELECT u.*, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.country = 'KR'
GROUP BY u.id;

修正:

CREATE INDEX idx_orders_user_id ON orders(user_id);

8.3 遅いJSONクエリ

CREATE INDEX idx_events_metadata ON events USING gin(metadata);

8.4 複合 vs 単一

WHERE a=? AND b=? AND c=? のとき、単一インデックス3個をBitmapAndするより (a, b, c) 複合1個が大抵速い。


9. インデックス保守

9.1 未使用インデックスの検出

SELECT
  schemaname || '.' || relname AS table,
  indexrelname AS unused_index,
  pg_size_pretty(pg_relation_size(indexrelid)) AS size,
  idx_scan AS index_scans
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

9.2 インデックス膨張

REINDEX INDEX CONCURRENTLY idx_users_email;

9.3 冗長インデックス

idx_ab(a, b) があれば idx_a(a) は削除可(leftmost rule)。

9.4 監視

SELECT schemaname, relname, indexrelname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;

10. MySQL vs PostgreSQL

特性MySQL (InnoDB)PostgreSQL
既定インデックスB+TreeB-Tree
クラスタインデックスあり (PRIMARY KEY)なし
Hash IndexMemoryエンジンのみあり
全文検索FULLTEXTtsvector + GIN
JSONインデックスGenerated columnsGIN on JSONB
Partial Index限定的 (8.0+)あり
Expression Index8.0+あり
Covering Indexありあり (INCLUDE)
BRINなしあり
GiSTなしあり

クイズ

1. 複合インデックス (a, b, c) で使えるクエリは?

: leftmost prefix rule — 可: WHERE a=?WHERE a=? AND b=?WHERE a=? AND b=? AND c=?、部分的に WHERE a=? AND c=?。不可: WHERE b=?WHERE c=?WHERE b=? AND c=?

2. GIN Indexが適する場面は?

: 多値カラム — 配列 tags @> ARRAY['redis']、JSONB metadata @> '{"key": "value"}'、全文検索 tsvector @@ tsquerypg_trgm による先頭ワイルドカードLIKE高速化。

3. 推定と実測で行数が1000倍ずれる場合は?

: 統計が不正確。ANALYZE を実行し default_statistics_target を上げる(100 -> 1000)、列ごとに SET STATISTICS で調整。autovacuumの analyze_scale_factor を下げるのも有効。

4. Partial Indexの利点は?

: インデックスサイズ縮小、高速ビルド/保守、キャッシュ効率向上。例: CREATE INDEX idx_active ON users(email) WHERE status='active' はアクティブユーザーのみ索引化。

5. Index Only Scanが通常のIndex Scanより速い理由は?

: 通常のIndex Scanは ctid を取得してヒープを読むが、Index Only Scanは必要な列が全て索引に含まれる(INCLUDE 句)のでテーブルアクセス不要。visibility mapが必要なのでVACUUMが前提。


参考資料

현재 단락 (1/236)

- **B-Treeが90%**: 多くのクエリに適合。ソート、範囲、完全一致を全て支援。

작성 글자: 0원문 글자: 9,162작성 단락: 0/236