LESSON 30分

ストーリー

「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-TreeHash
等価検索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等価検索専用完全一致のみ
GINJSONB、配列、全文検索包含、全文検索

理解度チェックリスト

  • B-Tree インデックスの構造と検索原理を説明できる
  • 複合インデックスのカラム順序の重要性を理解している
  • GIN インデックスの用途を説明できる
  • インデックスのメリット・デメリットを判断できる

次のステップ

次のレッスンでは**実行計画(EXPLAIN ANALYZE)**の読み方を学ぶ。インデックスが本当に使われているか、クエリがどう実行されるかを可視化する技術だ。


推定読了時間: 30分