この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:3
ページ更新者:guest
更新日時:2026-06-11 07:07:02

タイトル: 索引(インデックス)
SEOタイトル: DB Index 完全ガイド (B-Tree / Hash / Bitmap / GIN / Covering)

この記事の要点
  • Index は SELECT を速くする一方、INSERT/UPDATE/DELETE を遅くする トレードオフがある
  • B-Tree がデフォルト。範囲検索・ソート・前方一致 LIKE に強い
  • Composite Index は左端から使われる (Leftmost Prefix Rule)
  • Covering Index はクエリで必要な列を全部含むインデックス → Index-Only Scan で高速
  • カーディナリティ低い列 (性別、フラグ等) には Bitmap Index またはインデックス不要

Index (索引) とは

DB の Index は、テーブルから特定の行を高速に見つけるためのデータ構造です。本の巻末索引と同じで、「email = ? で 1 件取りたい」というクエリに対し、全行 (Full Table Scan) ではなく Index 経由で対数時間 O(log N) で到達できます。

ただし万能ではなく、INSERT / UPDATE / DELETE 時に Index 自身も更新されるため、書き込みヘビーなシステムでは逆効果になる場合もあります。「むやみに全列に Index 張る」は典型的なアンチパターンです。

主要な Index 種別

種類得意代表 DB
B-Tree等価 / 範囲 / ソート / 前方一致 LIKE★ 全 DB のデフォルト
Hash等価のみ (高速)MySQL Memory, PostgreSQL
Bitmapカーディナリティ低い列、OLAPOracle, PostgreSQL (実装は GIN/BRIN)
Full Text自然言語の全文検索MySQL, PostgreSQL (tsvector)
GiST / GIN配列 / JSON / 地理空間PostgreSQL
R-Tree / SP-GiST地理空間 / 多次元PostgreSQL, SQLite
Cluster (Index Organized Table)主キー順に物理配置InnoDB の Primary Key, Oracle IOT

B-Tree Index の仕組み

                  [50]
                 /     \
            [20|35]    [70|85]
           /   |   \   /  |  \
      [10] [25,30] [40] [60] [75,80] [90,95]

特徴:
- 内部ノードは範囲、葉ノードは実データ (or 行 ID) へのポインタ
- 深さ 3-4 で数億行をカバー (平衡木)
- 範囲検索: 葉ノードを横にリンクで辿る (Linked List)
- 前方一致 LIKE 'abc%' は使えるが LIKE '%abc' は使えない

Index の作成・確認

-- 単一列
CREATE INDEX idx_users_email ON users(email);

-- 複合 (Composite) Index
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

-- ユニーク
CREATE UNIQUE INDEX uq_users_login_id ON users(login_id);

-- 部分 Index (PostgreSQL / SQL Server)
CREATE INDEX idx_orders_active ON orders(user_id)
WHERE status = 'active';

-- 関数 Index (式)
CREATE INDEX idx_lower_email ON users(LOWER(email));

-- 降順
CREATE INDEX idx_logs_date_desc ON logs(created_at DESC);

-- 削除
DROP INDEX idx_users_email;       -- PostgreSQL / Oracle
ALTER TABLE users DROP INDEX idx_users_email;  -- MySQL

Composite Index と Leftmost Prefix Rule

複数列 Index は左端から順にのみ使われます。これは B-Tree が複合キーをタプル順に並べるためです:

-- インデックス: (a, b, c)
CREATE INDEX idx_abc ON tbl(a, b, c);

-- ✅ 使える
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 AND c = 3                -- 部分的 (a だけ使用、c はフィルタ)

-- ❌ 使えない (a が条件に無い)
WHERE b = 2
WHERE b = 2 AND c = 3
WHERE c = 3

列順はカーディナリティ高い → 低い順 + 検索パターンで決定。「絞り込み量の多い列」を先頭にするのが基本。

Covering Index と Index-Only Scan

クエリで使う列 (SELECT / WHERE / ORDER BY) が全て Index に含まれていれば、テーブル本体を読まずに Index だけで結果を返せます (Index-Only Scan):

-- テーブル
CREATE TABLE orders (id, user_id, status, total, created_at, ...);

-- ❌ Covering でない
CREATE INDEX idx_user ON orders(user_id);
SELECT total FROM orders WHERE user_id = 100;  -- Index → 行 → total 取得

-- ✅ Covering (PostgreSQL の INCLUDE)
CREATE INDEX idx_user_cov ON orders(user_id) INCLUDE (total);
SELECT total FROM orders WHERE user_id = 100;  -- Index だけで完結

-- ✅ MySQL では INCLUDE が無いので順序を工夫
CREATE INDEX idx_user_cov ON orders(user_id, total);
SELECT total FROM orders WHERE user_id = 100;

EXPLAIN で Index 利用を確認

-- MySQL / PostgreSQL
EXPLAIN SELECT * FROM users WHERE email = 'foo@example.com';

-- PostgreSQL の実測値
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'foo@example.com';

-- 注目ポイント
-- Seq Scan (Postgres) / type=ALL (MySQL) → Full Scan、Index 効いてない
-- Index Scan / type=ref or range          → Index 使用
-- Index Only Scan                         → Covering Index 効いてる
-- rows=10  cost=...                       → 推定行数とコスト

カーディナリティと Index Selectivity

カーディナリティ (Cardinality) = 列に含まれるユニーク値の数。Selectivity = カーディナリティ / 全行数。

列の種類カーディナリティSelectivityB-Tree Index 効果
email, uuid非常に高い1.0 近い★ 抜群
user_id高い0.1 以上★ 効く
status (5 値)低い0.0005状況次第 (Partial / Bitmap 検討)
gender, flag2 値0.5無効 (Full Scan が速い)

Partial Index / 部分インデックス

「全データの 1% にしかヒットしない」場合、その 1% だけを Index に含めると容量も保守コストも激減:

-- PostgreSQL
CREATE INDEX idx_orders_pending ON orders(created_at)
WHERE status = 'pending';   -- pending 行だけ

-- 効果
-- ・Index サイズ激減 (全 1000 万 → 1 万)
-- ・INSERT/UPDATE 時の Index 更新コスト減
-- ・status = 'pending' を含むクエリでのみ自動利用

Bitmap Index (Oracle)

カーディナリティの低い列に対し、各値ごとに「該当行=1, それ以外=0」のビットマップで持つ。AND/OR をビット演算で実行でき DWH の集計に強い:

-- Oracle のみ
CREATE BITMAP INDEX idx_status_bm ON orders(status);

-- 適用条件
-- ・カーディナリティ < 全行数の 1% 程度
-- ・読み取り中心 (DWH / 分析)
-- ・OLTP には不向き (同時更新でロック広範囲)

Full Text Index

-- MySQL
CREATE FULLTEXT INDEX idx_ft_body ON articles(body) WITH PARSER ngram;
SELECT * FROM articles
WHERE MATCH(body) AGAINST('PostgreSQL 速い' IN NATURAL LANGUAGE MODE);

-- PostgreSQL (tsvector)
CREATE INDEX idx_ft_body ON articles USING GIN (to_tsvector('english', body));
SELECT * FROM articles
WHERE to_tsvector('english', body) @@ to_tsquery('postgres & fast');

Cluster Index (Index Organized Table)

テーブル本体が Index の葉ノードに直接格納される形式。InnoDB は主キーが必ず Cluster Index になります:

  • 主キー検索が高速 (1 回の Index 探索でデータ到達)
  • 主キー以外の Index (Secondary Index) は「主キー値」を持ち、2 段階探索になる
  • 主キーは狭く・連番が望ましい (UUID v4 を主キーにすると断片化激しい)

Index 維持コスト

操作Index 無しIndex N 個
SELECT (該当列)遅い (Full Scan)★ 速い
INSERT速い遅い (N 個 B-Tree 更新)
UPDATE (Index 列以外)普通普通
UPDATE (Index 列)普通遅い (古エントリ削除 + 新規)
DELETE普通遅い (N 個更新)
ディスク容量2-3 倍に膨らむ事も

Index 設計のチェックリスト

  1. 主キー / 外部キーには必ず Index
  2. WHERE / JOIN / ORDER BY の主要列を Index 化
  3. Composite Index は左端順に注意
  4. カーディナリティが低い列の単独 Index は避ける
  5. Covering Index で Index-Only Scan を狙う
  6. 不要 Index は pg_stat_user_indexes / sys.dm_db_index_usage_stats で発見して削除
  7. 本番では CREATE INDEX CONCURRENTLY (PostgreSQL) で無停止作成

FAQ

Q: Index を貼ったのに使われない
A: 統計情報が古い (ANALYZE 実行)、列の型が条件と違う (暗黙変換)、関数を当てている (WHERE YEAR(date) = 2024 は使えない)、選択率が悪い (全行の 30% ヒットなら Full Scan の方が速い) など。

Q: 主キーと Unique Index の違い
A: 主キーは「テーブルに 1 つ、NULL 不可」、Unique Index は「複数可、NULL は 1 行のみ許容 (DB により挙動差)」。両方ともユニーク制約 + Index の機能を持ちます。

Q: 何個まで Index を貼って良い?
A: 目安はテーブル当たり 5-7 個。書込頻度が高いなら 3 個程度。多すぎる場合はクエリパターン整理を先に。

Q: Index Rebuild は必要?
A: PostgreSQL では REINDEX、MySQL InnoDB では OPTIMIZE TABLE で断片化解消。AutoVacuum / ALTER TABLE Online がある現代では年単位の保守で十分。