概要(インデックスは「データベースの辞書の“索引”」)
インデックス(index)は、データベースにとっての「本の索引」です。
本の後ろにある五十音順の索引があると、目的の用語のページを一瞬で開けますよね。
インデックスは、まさにそれと同じことをテーブルの中でやっています。
インデックスがないと、データベースは「最初のページから最後のページまで全部めくりながら探す」ことになります。
インデックスがあると、「索引を見て、該当ページに一気にジャンプ」できます。
ここから、
インデックスが何をしているのか
どんなときに効くのか
どんなときに逆効果になるのか
を、初心者向けにかみ砕いて説明していきます。
まずは「インデックスなし」の世界をイメージする
全件なめる検索(フルスキャン)のイメージ
users テーブルを考えます。
users テーブル(100 万件あるとする)
id | name | email
---+-----------+----------------------
1 | Taro | taro@example.com
2 | Hanako | hanako@example.com
...
ここで、次のクエリを実行するとします。
SELECT *
FROM users
WHERE email = 'taro@example.com';
SQLインデックスがない場合、データベースはどうするか。
イメージとしてはこうです。
1 行目を見る → email が一致するか? → 違う
2 行目を見る → 一致するか? → 違う
…
100 万行目まで、ひたすらチェック
つまり、「全件を順番にチェックする」しかありません。
これをフルテーブルスキャン(全表走査)と呼びます。
データが少ないうちは問題になりませんが、
何十万件、何百万件と増えてくると、
この「全部なめる」方式は一気に重くなります。
本の索引がないとどうなるか、に置き換えて考える
本の中から「インデックス」という単語が出てくるページを探したいとします。
索引がなければ、1 ページ目から最後まで全部めくって探すしかありません。
これが「インデックスなしの WHERE 検索」です。
「ページ数が少ない本」ならまだいいですが、
「1000 ページある専門書」だと地獄ですよね。
データベースも同じで、
行数が増えるほど「索引の有無」が効いてきます。
インデックスありの世界(索引で一気にジャンプ)
email にインデックスを張ると何が変わるか
さきほどの users.email にインデックスを作ってみます。
CREATE INDEX idx_users_email
ON users(email);
SQLこれで、users テーブルとは別に、
「email だけをキーにした索引」が作られます。
イメージとしては、こんな表が裏でできる感じです。
インデックス(email → 行の場所)
email | 行の場所
--------------------+---------
hanako@example.com | users の 2 行目
taro@example.com | users の 1 行目
...
実際にはもっと賢い木構造(B-tree など)で管理されていますが、
初心者のうちは「email と、その行がどこにあるかの対応表」と思っておけば十分です。
この状態で、さきほどのクエリを実行するとどうなるか。
SELECT *
FROM users
WHERE email = 'taro@example.com';
SQLデータベースは、まずインデックスを見ます。
インデックスの中から ‘taro@example.com’ を探す
対応する「行の場所」を知る
その場所に一気にジャンプして、users の該当行だけ読む
つまり、「全部の行をなめる」のではなく、
「索引を見て、必要な行だけをピンポイントで読む」動きになります。
これが、インデックスの本質です。
インデックスが効く典型パターン
WHERE でよく絞り込みに使う列
一番分かりやすいのは、WHERE で頻繁に使う列です。
例えば、次のようなクエリをよく打つとします。
SELECT *
FROM users
WHERE email = '...';
SQLこの場合、email にインデックスがあると、
毎回の検索が「索引ジャンプ」になり、
フルスキャンより圧倒的に速くなります。
同じように、
ユーザー ID で絞る
日付で絞る
ステータスで絞る
といった「よく条件に使う列」は、
インデックス候補になります。
JOIN の ON で使う列
JOIN でもインデックスは重要です。
例えば、users と orders を JOIN するとき、
SELECT ...
FROM users
JOIN orders
ON users.id = orders.user_id;
SQLorders.user_id にインデックスがあると、
JOIN のときに「対応する行を探す」処理が速くなります。
外部キーとして他テーブルを参照している列(user_id, product_id など)は、
JOIN で頻繁に使われるので、
インデックスを張っておくのが定石です。
ORDER BY や GROUP BY にも効くことがある
ORDER BY や GROUP BY でも、
インデックスがうまく使われると速くなります。
例えば、
SELECT *
FROM users
ORDER BY created_at DESC
LIMIT 10;
SQLcreated_at にインデックスがあると、
「新しい順に並んだ状態の索引」から先頭 10 件を取る、
という形で処理できることがあります。
ただし、ORDER BY とインデックスの関係は少し奥が深いので、
最初は「WHERE や JOIN で効く」と覚えておけば十分です。
インデックスの「代償」もちゃんと理解する
インデックスは「タダで速くなる魔法」ではない
ここがとても大事なポイントです。
インデックスは検索を速くしてくれますが、
その代わりに「書き込み(INSERT / UPDATE / DELETE)が重くなる」という副作用があります。
なぜかというと、
新しい行を INSERT したら、テーブルだけでなくインデックスにも登録しなければならない
既存の行のインデックス対象列を UPDATE したら、インデックスの中身も更新しなければならない
行を DELETE したら、インデックスからも削除しなければならない
からです。
つまり、
インデックスが多いほど、
「書き込みのたびにやること」が増える
ということになります。
「何でもかんでもインデックス」は逆効果
よくある失敗パターンが、
「検索が遅い → とりあえず全部の列にインデックスを張る」
というやり方です。
インデックスは 1 個 1 個が「索引用のデータ構造」なので、
数が増えるほど、
ディスク容量を食う
INSERT / UPDATE / DELETE が重くなる
というデメリットが積み上がります。
だからこそ、
本当に検索や JOIN でよく使う列だけに絞ってインデックスを張る
という「選別」がとても重要になります。
単一カラムインデックスと複合インデックス
単一カラムインデックス(1 列だけの索引)
これまでの例は、すべて「1 列だけのインデックス」でした。
CREATE INDEX idx_users_email
ON users(email);
SQLこれは、「email だけをキーにした索引」です。
WHERE email = ‘…’
WHERE email LIKE ‘abc%’
のようなクエリで効きます。
複合インデックス(複数列をまとめた索引)
現場では、「複数の列の組み合わせ」で検索することも多いです。
例えば、次のようなクエリをよく使うとします。
SELECT *
FROM orders
WHERE user_id = 1
AND status = 'paid';
SQLこのとき、複合インデックスを張ることができます。
CREATE INDEX idx_orders_user_status
ON orders(user_id, status);
SQLこれは、「user_id → status の順で並んだ索引」です。
ここで重要なのが、「左から順に効く」という性質です。
このインデックスは、
WHERE user_id = ?
WHERE user_id = ? AND status = ?
には効きますが、
WHERE status = ?
だけだと効きにくい(または効かない)ことが多いです。
複合インデックスは、
左から順に条件に使われるときに最大限力を発揮する
という性質を持っています。
インデックスが効きにくい・効かないパターン
先頭に % が付いた LIKE
例えば、email にインデックスがあるとして、
WHERE email LIKE 'taro%'
SQLこれは、「taro で始まるメールアドレス」を探すクエリです。
多くのデータベースでは、インデックスをうまく使えます。
一方、
WHERE email LIKE '%@example.com'
SQLのように、先頭に % が付いていると、
「どこに @example.com が出てくるか分からない」ため、
インデックスが効きにくくなります。
本の索引で、「3 文字目が ‘a’ の単語を探せ」と言われるようなものです。
先頭が決まっていないと、索引の構造を活かしにくいのです。
計算や関数をかけた列
例えば、created_at にインデックスがあるとして、
WHERE created_at >= '2025-01-01'
SQLこれはインデックスが効きやすいです。
一方、
WHERE DATE(created_at) = '2025-01-01'
SQLのように、列に関数をかけてしまうと、
インデックスが効かなくなることがあります。
created_at の生の値ではなく、
DATE(created_at) という「変換後の値」で比較しているからです。
最初のうちは、「インデックス列に関数をかけると遅くなりがち」と覚えておけば十分です。
実務的な「インデックスを考える順番」
まずは「どんなクエリが多いか」を見る
インデックスを考えるとき、
いきなり「どの列に張ろうかな」と考えるのではなく、
どんな SELECT がよく実行されているか
どんな WHERE 条件が多いか
どんな JOIN が多いか
という「クエリ側」から考えるのが筋が良いです。
アプリのコードを見て、
ユーザーを email でよく検索している
注文を user_id と status でよく絞っている
日付範囲での検索が多い
といったパターンを洗い出し、
それに合わせてインデックスを設計します。
「遅いクエリ」を見つけてから、ピンポイントで張る
いきなり全部にインデックスを張るのではなく、
実際に遅くなっているクエリ
今後確実に重くなりそうなクエリ
を特定してから、
そのクエリに効くインデックスをピンポイントで張る、
というアプローチが現実的です。
そのとき、
WHERE で使っている列
JOIN の ON で使っている列
ORDER BY / GROUP BY で使っている列
を中心に考えると、
「効くインデックス」を設計しやすくなります。
まとめ(インデックスは「速さ」と「重さ」のトレードオフを設計する道具」)
インデックスを初心者目線で整理すると、こうなります。
インデックスは「テーブルとは別に持つ索引」で、WHERE や JOIN での検索を速くするための仕組み。
よく条件に使う列(email、user_id、日付など)にインデックスを張ると、フルスキャンせずに「索引ジャンプ」で探せるようになる。
その代わり、INSERT / UPDATE / DELETE のたびにインデックスも更新する必要があるので、「書き込みは重くなる」「ディスクも食う」というコストがある。
複合インデックスは「左から順に効く」ので、クエリの WHERE 条件の書き方とセットで設計する必要がある。
何でもかんでも張るのではなく、「実際によく使うクエリ」「遅くなっているクエリ」から逆算して、必要なものだけを選ぶのが大事。
