「開発環境では一瞬で返るのに、本番でデータが増えたらクエリが数秒かかるようになった」——DB起因の性能問題は、たいていリリース後しばらくしてから顕在化します。そして原因の多くは、インデックスが張られていないか、張ってあるのに使われていないかのどちらかです。
この記事では、B-treeインデックスの基本から、複合インデックスの列順の決め方、EXPLAINの読み方、そして「張ったのに効かない」典型パターンまでを整理します。
B-treeインデックスの基本イメージ
RDBMSのインデックスはほとんどがB-tree構造です。ソート済みの索引を木構造でたどるため、全行を走査する代わりに対数オーダーで目的の行に到達できます。書籍の索引と同じで、「値で探す」「範囲で探す」「ソート済みで取り出す」操作が得意です。
一方でインデックスはタダではありません。INSERT/UPDATE/DELETEのたびに索引の更新コストがかかり、ストレージも消費します。「とりあえず全列に張る」は書き込み性能を落とすだけなので、WHERE・JOIN・ORDER BYで実際に使われる列に絞って設計します。
複合インデックスは「列の順番」がすべて
複合インデックスは先頭列から順に使われます。電話帳が「姓→名」の順で並んでいるのと同じで、(last_name, first_name) の索引は姓だけの検索には使えますが、名だけの検索には使えません。
列順を決める実務上の指針は次の通りです。
- 等値条件(=)で使う列を先頭に、範囲条件(>、BETWEEN)の列は後ろに置く
- 範囲条件の列より後ろの列は、絞り込みには使われないことを意識する
- ORDER BYの列を末尾に含めると、ソート処理自体を省略できることがある
-- status で等値、created_at で範囲+ソートなら
CREATE INDEX idx_orders_status_created
ON orders (status, created_at);
SELECT * FROM orders
WHERE status = 'pending'
AND created_at >= '2026-06-01'
ORDER BY created_at;
逆順の (created_at, status) にすると、範囲条件が先頭に来るためstatusでの絞り込みに索引を活かせません。この違いだけで実行時間が桁で変わることがあります。
EXPLAINで「想定通りか」を必ず確認する
インデックスは張って終わりではなく、実際に使われているかをEXPLAINで確認します。MySQLの場合、最低限見るべきは次の項目です。
- type:
ALL(フルスキャン)が出ていたら要注意。refやrangeなら索引を利用 - key: 実際に選ばれたインデックス。想定と違う索引が選ばれることもある
- rows: 読む見込みの行数。実データ量に対して極端に多ければ設計を見直す
- Extra:
Using filesortやUsing temporaryはソート・一時表のコストがかかっているサイン
本番相当のデータ量で確認するのが重要です。行数が少ないとオプティマイザがあえてフルスキャンを選ぶため、開発環境のEXPLAIN結果は当てにならないことがあります。
効かないインデックスの典型パターン
「張ってあるのに使われない」場合、だいたい次のどれかに当てはまります。
- 索引列への関数適用:
WHERE DATE(created_at) = '2026-07-01'は不可。created_at >= '2026-07-01' AND created_at < '2026-07-02'に書き換える - 暗黙の型変換: 文字列列に数値で比較(
WHERE code = 123)すると索引が無効になる - 前方一致でないLIKE:
LIKE '%keyword%'はB-treeでは効かない。全文検索の導入を検討 - 絞り込み効果の低い列: 値が2〜3種類しかない列の単独インデックスは選ばれにくい
- OR条件の組み合わせ: 索引が使えない形になりやすく、UNIONへの分解が有効な場合がある
まとめ
インデックス設計は「クエリありき」です。まず実際のWHERE・JOIN・ORDER BYを洗い出し、等値→範囲→ソートの原則で複合インデックスの列順を決め、本番相当データのEXPLAINで検証する。この流れを新機能のリリース前チェックに組み込んでおくと、「データが増えてから発覚する性能問題」をかなりの割合で未然に防げます。
