「とりあえず、インデックス。」を卒業する
1. インデックスは「魔法」じゃない
「SQLが遅い? じゃあインデックスを貼ろう」
…そんな風に、おまじない感覚でインデックスを使っていませんか?
-- 有効なユーザーだけ素早く表示したい!
-- is_active カラムに INDEX を貼ろう!!
「.....」
こうならないために、この記事を書きました。
インデックスの正体は 「特定の順番できれいに並べられた名簿」 です。
DBの中には、この名簿を管理する 「DBくん」 がいます。
彼に 「ワープ(索引ジャンプ)」 をさせる指示を出さないと、インデックスはただの 「重い飾り」 になってしまいます。
では、DBくんにワープさせるにはどうすればいいか。まずはその鉄則から見ていきます。
2. DBくんに「ワープ」をさせる3つの鉄則
インデックスを効かせるための法則はたった一つです。
「WHERE句の左側(カラム名)を、加工せずに裸のままで置く」
カラムに手を加えた瞬間、DBくんは名簿を使えなくなります。具体的に見てみましょう。
① 計算をさせない
-- ❌ NG
WHERE price * 1.1 > 1000
DBくん:「名簿には元の値段しかないよ!全員分1.1倍して計算し直さないとわからないじゃん。もういい、最初から全部めくるわ(フルスキャン)!」
計算はSQL側で先に済ませて、結果だけ渡してあげましょう。
-- ✅ OK
WHERE price > 909
DBくん:「先に答えを教えてくれたんだね。それなら名簿のその場所まで一気にワープできるよ!」
② 関数で包まない
日付の検索でよくやりがちなパターンです。
-- ❌ NG
WHERE MONTH(birthday) = 10
DBくん:「名簿は年月日順なんだ。10月生まれは各年にバラバラにいるからワープできないよ。全員分『何月生まれ?』って聞いて回るしかない…」
関数を使った瞬間、名簿の並び順が意味をなさなくなります。範囲で渡すのが正解です。
-- ✅ OK
WHERE birthday BETWEEN '2023-10-01' AND '2023-10-31'
DBくん:「10月の範囲だね!その期間の開始地点にワープして、終わるまでをガバッと取るだけだから一瞬だよ!」
③「後ろ」の検索はワープできない
-- ❌ NG:後方一致・中間一致
WHERE name LIKE '%田中'
-- ✅ OK:前方一致
WHERE name LIKE '田中%'
DBくん:「名簿は『あいうえお順』。先頭がわかればワープできるけど、お尻が『田中』の人なんてどこにいるか見当もつかないよ。最初から全部見るね。」
この3つを守れば、インデックスはちゃんと機能します。では、なぜ守るとワープできるのか。その理由がインデックスの内部構造にあります。
3. なぜワープできるのか? Bツリーの正体
インデックスは単なるリストではなく、「Bツリー」 という木の形をした構造をしています。
頂点(ルート)から「あっち、こっち」と枝分かれを数回たどるだけで、数百万件の中から目的のデータに辿り着けます。データが100万件あっても、たった20回程度のチェックで済む(計算量O(log n))のが爆速の理由です。
カラムを関数や計算で加工してしまうと、このBツリーの「どこにジャンプすればいいか」という情報が失われます。だからDBくんはやむなく最初から全件をめくる羽目になるわけです。
さらに速くする「カバリングインデックス」
通常、DBくんは名簿で場所を見つけた後、さらに 「テーブル本体」 まで中身を確認しに行きます(テーブルアクセス)。
しかし、もしインデックスの中にSELECTしたいカラムも含まれていたら?
DBくん:「名簿の中に全部書いてあるじゃん!テーブル本体を見に行かなくて済むから、さらに速く返せるよ!(Index Only Scan)」
これが カバリングインデックス です。SELECTするカラムをインデックスに含めることで、テーブルアクセスを省略できます。
さて、ここまで「正しく書けばインデックスは効く」という話をしてきました。ただ、実は 正しく書いたとしても、インデックスが使われないことがあります。 インデックスを使うかどうかを最終的に判断するのは、DBくん自身だからです。
4. EXPLAINで答え合わせ
DBには 「オプティマイザ」 という仕組みが存在します。SQLの実行時に「どうやって処理するのが一番速いか」を判断し、実行計画を決めるものです。EXPLAINとは、そのオプティマイザの思考回路を覗くためのツールです。
EXPLAIN SELECT * FROM users WHERE is_active = 1;
オプティマイザが「フルスキャンの方が速い」と判断するケース
インデックスを貼っていても、オプティマイザが使わないと判断する場合があります。代表的なケースは2つです。
① カーディナリティが低い
カーディナリティとは「そのカラムに何種類の値があるか」です。
| カラム | 値の種類 | カーディナリティ |
|---|---|---|
user_id(主キー) |
全員バラバラ | 高い ✅ |
is_active |
0か1だけ | 低い ⚠️ |
is_activeのように値の種類が少ないカラムは、インデックスを使っても結局ほぼ全件ヒットします。
DBくん:「どうせほぼ全件ヒットするなら、最初から全部めくった方が早いわ」
② テーブルのレコード数が少ない
数十〜数百行程度の小さいテーブルでは、フルスキャンの方が速いと判断されます。
typeで評価する
EXPLAINの結果で最初に確認すべきは type カラムです。
| typeの値 | 評価 | DBくんの動き |
|---|---|---|
ALL |
❌ 絶望 | インデックスを無視して全データをめくっている |
index |
🔺 微妙 | インデックスは使っているが端から端まで全部見ている |
range |
✅ 合格! | 特定の範囲をワープして探せている |
ref |
🏆 優秀 | インデックスで複数行ヒット |
eq_ref |
🏆 優秀 | JOINで1行確定ヒット |
const |
🏆 最高! | ピンポイントで1行確定。最速 |
range以上を目指しましょう。
ここまでは単一カラムへのインデックスの話でした。現場では複数カラムを組み合わせた 「複合インデックス」 を使う場面も多くあります。複合インデックスには、単体とは異なるルールがあります。
5. 複合インデックスの列順序
複合インデックスは 「電話帳」 に例えられます。電話帳が [名字 → 名前] の順で並んでいるように、左から順番に使わないとワープできません。
INDEX idx_sample (status, user_id, updated_at)
-- ✅ 効く
WHERE status = 1
WHERE status = 1 AND user_id = 100
WHERE status = 1 AND user_id = 100 AND updated_at > '2024-01-01'
-- ❌ 効かない
WHERE user_id = 100 -- 先頭のstatusを飛ばしている
WHERE updated_at > '2024-01-01' -- 先頭を飛ばしている
「健さんを探したい」→ 名字がわからないと電話帳を最初から全部めくるしかない
設計のコツ:カーディナリティが低いカラムの活かし方
先ほど「カーディナリティが低いカラム単体のインデックスは意味がない」という話をしました。しかし、複合インデックスの一部として使うなら話は変わります。
-- ❌ is_active 単体 → ほぼ無意味
INDEX (is_active)
WHERE is_active = 0 -- カーディナリティ低い → どうせフルスキャン
-- ❌ is_activeを先頭にしても意味がない
INDEX (is_active, created_at)
-- is_active単体クエリはどうせフルスキャンになるので先頭にする意味がない
-- ✅ カーディナリティ高いcreated_atを先頭にする
INDEX (created_at, is_active)
WHERE created_at >= '2026-01-01' AND is_active = 0
DBくん:「created_atで一気に絞って、さらにis_activeで絞れた!フルスキャンより全然速いよ!」
is_active単体のクエリはどうせカーディナリティが低くてフルスキャンになるので、先頭にこだわる必要はありません。カーディナリティが高い方を先頭に置く方が絞り込み効率が上がります。
列順序を決めるときは以下を意識しましょう。
① カーディナリティが高いカラムを先頭に置く
② 範囲検索(BETWEEN, >, <)のカラムは一番右に置く
6. まとめ
「インデックスを貼ったのに速くならない」時、それはDBくんに無茶振りをしているサインです。
- ✅ カラムを加工しない(計算・関数は右側に寄せる)
- ✅ 型を合わせる(暗黙の型変換をさせない)
- ✅ 前方一致を心がける
- ✅ カーディナリティの低いカラム単体にインデックスを貼らない
- ✅ カーディナリティの低いカラムは複合インデックスで活かす
- ✅ 複合インデックスはカーディナリティが高いカラムを先頭に、範囲検索カラムを右端に
- ✅ EXPLAINで
type: range以上になっているか確認する
インデックスは、ディスクI/O(読み込みコスト)を最小化するためのツールです。
「DBくんがワープしやすい形」に条件を整えてあげること。
これを意識してみましょうー!