「クエリが遅い」「DBサーバーの負荷が高い」——開発現場でこんな問題に直面したことはありませんか?
筆者が過去に担当したECサイトでは、注文一覧の取得に8秒以上かかっていたクエリが、インデックスの見直しだけで0.05秒未満に改善した経験があります。コードを1行も変えずに、です。
この記事を読み終えれば、EXPLAINコマンドを使って自分でボトルネックを特定し、根拠のあるインデックス設計ができるようになります。「なんとなく設定」から卒業しましょう。
まず「EXPLAIN」でボトルネックを特定する
インデックスを闇雲に追加する前に、現状把握が最優先です。MySQLやPostgreSQLにはEXPLAINコマンドが用意されており、クエリがどのように実行されているかを確認できます。
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND order_date >= '2024-01-01';
出力結果のどこを見ればよいか、重要な項目を絞って紹介します。
| 項目 | 見るべき内容 |
|---|---|
type |
ALL(フルスキャン)は要注意。refやrangeを目指す |
key |
実際に使用されているインデックス名。NULLならインデックス未使用 |
rows |
スキャン行数の推定値。少ないほど良い |
Extra |
Using indexは高速の証。Using filesortはソート処理が発生している証拠 |
type: ALLかつrowsが数十万以上なら、インデックスの設計を見直す余地が大いにあります。
インデックスの基本:なぜ速くなるのか
インデックスとは、テーブルのデータを高速に検索するための「索引」です。インデックスがない場合、DBはテーブルの全行を先頭から順番に走査します(フルテーブルスキャン)。データが数百万件になると、これだけで数秒〜数十秒かかることがあります。
インデックスを設定すると、DBは目的のデータに直接アクセスできるようになり、検索速度が劇的に向上します。
代表的なインデックスの種類
B-Treeインデックス(デフォルト) MySQL・PostgreSQL・Oracleなど主要なRDBMSでデフォルトとして使われます。木構造でデータを管理し、等値検索・範囲検索・ソートのすべてに対応できる万能型です。特別な理由がなければこれを使います。
CREATE INDEX idx_users_email ON users(email);
ハッシュインデックス
等値検索(=)に限定した超高速インデックスです。ただしBETWEENや>などの範囲検索には使えません。「IDによるピンポイント検索しか行わない」といった用途に限定されます。
複合インデックス 複数カラムを組み合わせたインデックスです。設計次第でパフォーマンスを大きく左右するため、後のセクションで詳しく解説します。
インデックスを設定すべきカラムの見極め方
インデックスは「多ければ多いほど良い」ものではありません。インデックスが増えるほどINSERT・UPDATE・DELETEのコストが増加し、ストレージ消費も増えます。以下の基準で対象を絞り込みましょう。
✅ インデックスに向いているカラム
WHERE句に頻繁に登場するカラム(例:status、user_id)JOIN条件に使われるカラム(外部キーなど)ORDER BYやGROUP BYに使われるカラム- カーディナリティ(値の種類数)が高いカラム(メールアドレス、UUIDなど)
❌ インデックスに向いていないカラム
- カーディナリティが低いカラム(
true/falseのフラグ、性別など) - 頻繁に更新されるカラム
- レコード数が数千件以下の小さなテーブルのカラム
複合インデックスの設計:カラム順序が命
複合インデックスの設計で最も重要なのが「カラムの順序」です。2つの原則を順番に理解しましょう。
原則①:左端一致(絶対条件)
複合インデックス(A, B, C)は、左端のカラムから順に使われる場合にのみ有効です。
-- ✅ 有効:左端のAを含む WHERE A = 1 WHERE A = 1 AND B = 2 WHERE A = 1 AND B = 2 AND C = 3 -- ❌ 無効:左端のAが抜けている WHERE B = 2 WHERE B = 2 AND C = 3
原則②:カーディナリティの高い順(次の判断軸)
左端一致を守ったうえで、絞り込み効果の高いカラム(値の種類が多いカラム)を左に配置すると、インデックスの効率がさらに上がります。
-- ✅ 推奨:カーディナリティの高いuser_idを左に CREATE INDEX idx_orders_user_status ON orders(user_id, status); -- ⚠️ 非推奨:カーディナリティの低いstatusを左に CREATE INDEX idx_orders_status_user ON orders(status, user_id);
まず「どのカラムが左端に来るか」を決め、その後「カーディナリティ」で順序を微調整する、という判断フローで設計するとミスが減ります。
よくあるアンチパターンと対策
インデックスを設定しても「効いていない」ケースの多くは、以下のいずれかが原因です。
❌ インデックスカラムに関数を使う
-- ❌ 関数でラップするとインデックスが無効に WHERE YEAR(created_at) = 2024 -- ✅ 範囲検索に書き換える WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'
❌ 暗黙の型変換
-- ❌ カラムがINT型なのに文字列で比較すると型変換が発生 WHERE user_id = '100' -- ✅ 型を合わせる WHERE user_id = 100
❌ LIKE検索の後方・中間一致
-- ❌ 前方一致以外はインデックスが効かない WHERE name LIKE '%田中%' -- ✅ 前方一致ならインデックスが使える WHERE name LIKE '田中%'
中間・後方一致の全文検索が必要な場合は、MySQLのFULLTEXT INDEXやElasticsearchなど専用の仕組みを検討しましょう。
まとめ:今日から使えるインデックス設計の鉄則
| # | 鉄則 |
|---|---|
| 1 | まずEXPLAINでボトルネックを特定する |
| 2 | WHERE・JOIN・ORDER BYに使うカラムを優先的にインデックス化する |
| 3 | 複合インデックスは左端一致を絶対条件として、カーディナリティ順に並べる |
| 4 | カーディナリティの低いカラム単体にインデックスは不要 |
| 5 | インデックスカラムに関数・型変換を加えない |
| 6 | 定期的に不要なインデックスをSHOW INDEXやpg_stat_user_indexesで見直す |
インデックス設計に「魔法」はありません。しかし、EXPLAINで現状を把握し、上記の原則に従って設計するだけで、クエリの実行速度は確実に改善できます。まず手元のスロークエリにEXPLAINを打ち込むところから始めてみてください。