ストーリー
「100万レコードのテーブルから1件取得するのに3秒かかっています…」
あなたが悩んでいると、高橋アーキテクトが穏やかに答えた。
「インデックスを貼ってないのか。図書館で本を探すとき、背表紙を端から見ていくか? 索引を使うだろう? データベースも同じだ。インデックスは検索の高速化を担う、最も重要な物理設計要素だよ」
インデックスとは
インデックスは、テーブルのデータに対する「索引」。検索対象のカラム値とレコード位置のマッピングを保持し、フルテーブルスキャンを回避する。
インデックスなしの検索(Seq Scan)
テーブル: 100万行
検索: WHERE email = 'user@example.com'
→ 1行目から100万行目まで順番に比較
→ O(N) の計算量
インデックスありの検索(Index Scan)
B-Treeインデックス(email)
→ ツリーを辿って対象を特定
→ O(log N) の計算量
→ 100万行でも約20回の比較で到達
B-Tree インデックス
最も一般的なインデックス構造。等価検索、範囲検索、ソートに対応。
graph TD
Root["M"]
L1["D, H"]
R1["R, V"]
L2A["A-C"]
L2B["E-G"]
L2C["I-L"]
L2D["N-Q"]
L2E["S-U"]
L2F["W-Z"]
PTR["実際のデータへのポインタ"]
Root --> L1
Root --> R1
L1 --> L2A
L1 --> L2B
L1 --> L2C
R1 --> L2D
R1 --> L2E
R1 --> L2F
L2C --> PTR
作成と使用例
-- 単一カラムインデックス
CREATE INDEX idx_users_email ON users(email);
-- 複合インデックス(カラムの順序が重要)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- ユニークインデックス
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
B-Treeが効くクエリ
-- 等価検索
SELECT * FROM users WHERE email = 'user@example.com';
-- 範囲検索
SELECT * FROM orders WHERE created_at >= '2024-01-01';
-- プレフィックス検索
SELECT * FROM users WHERE name LIKE 'Taka%';
-- ソート
SELECT * FROM orders ORDER BY created_at DESC;
B-Treeが効かないクエリ
-- 後方一致(インデックスが使えない)
SELECT * FROM users WHERE name LIKE '%hash';
-- 関数適用(インデックスが使えない)
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
-- → 式インデックスで対処: CREATE INDEX idx_lower_email ON users(LOWER(email));
Hash インデックス
等価検索のみに特化。範囲検索やソートには使えない。
-- Hash インデックス(PostgreSQL)
CREATE INDEX idx_users_token ON users USING HASH(api_token);
-- 等価検索のみ高速
SELECT * FROM users WHERE api_token = 'abc123';
| 特性 | B-Tree | Hash |
|---|---|---|
| 等価検索 | O(log N) | O(1) |
| 範囲検索 | OK | 不可 |
| ソート | OK | 不可 |
| ディスクサイズ | やや大きい | 小さい |
| 用途 | 汎用 | トークン、ハッシュ値 |
GIN インデックス(Generalized Inverted Index)
配列、JSONB、全文検索に対応する転置インデックス。
-- JSONB に対するGINインデックス
CREATE INDEX idx_products_attrs ON products USING GIN(attributes);
-- 配列に対するGINインデックス
CREATE INDEX idx_articles_tags ON articles USING GIN(tags);
-- 全文検索
CREATE INDEX idx_articles_body ON articles
USING GIN(to_tsvector('english', body));
-- GINインデックスを活用するクエリ
SELECT * FROM products
WHERE attributes @> '{"color": "red"}';
SELECT * FROM articles
WHERE tags @> ARRAY['typescript', 'database'];
SELECT * FROM articles
WHERE to_tsvector('english', body) @@ to_tsquery('optimization');
複合インデックスの設計
カラム順序の原則
-- 複合インデックス: (A, B, C)
CREATE INDEX idx_composite ON orders(user_id, status, created_at);
-- 効くクエリ(左端からの連続)
WHERE user_id = 1 -- A
WHERE user_id = 1 AND status = 'paid' -- A, B
WHERE user_id = 1 AND status = 'paid' AND created_at > '2024-01-01' -- A, B, C
-- 効かないクエリ
WHERE status = 'paid' -- Bだけ(Aがない)
WHERE created_at > '2024-01-01' -- Cだけ
WHERE status = 'paid' AND created_at > '2024-01-01' -- B, C(Aがない)
TypeScriptでの表現
// Prismaでのインデックス定義
// schema.prisma
/*
model Order {
id Int @id @default(autoincrement())
userId Int
status String
createdAt DateTime @default(now())
@@index([userId, status, createdAt])
@@index([status])
}
*/
// クエリ: インデックスが効く
const paidOrders = await prisma.order.findMany({
where: {
userId: 1,
status: 'paid',
createdAt: { gte: new Date('2024-01-01') }
}
});
インデックスのコスト
| メリット | デメリット |
|---|---|
| SELECT が高速化 | INSERT/UPDATE/DELETE が遅くなる |
| ソートの高速化 | ディスク容量を消費 |
| ユニーク制約の実装 | メンテナンスコスト |
インデックスを貼るべきカラム
- WHERE句で頻繁に使われるカラム
- JOIN条件のカラム(FK)
- ORDER BY で使われるカラム
- UNIQUE制約が必要なカラム
インデックスが不要なケース
- テーブルの行数が少ない(数百行以下)
- カーディナリティが低い(true/false のような2値)
- ほとんどINSERTで検索が少ないテーブル
まとめ
| インデックス型 | 用途 | 対応するクエリ |
|---|---|---|
| B-Tree | 汎用 | 等価、範囲、ソート、プレフィックス |
| Hash | 等価検索専用 | 完全一致のみ |
| GIN | JSONB、配列、全文検索 | 包含、全文検索 |
理解度チェックリスト
- B-Tree インデックスの構造と検索原理を説明できる
- 複合インデックスのカラム順序の重要性を理解している
- GIN インデックスの用途を説明できる
- インデックスのメリット・デメリットを判断できる
次のステップ
次のレッスンでは**実行計画(EXPLAIN ANALYZE)**の読み方を学ぶ。インデックスが本当に使われているか、クエリがどう実行されるかを可視化する技術だ。
推定読了時間: 30分