インデックスで速くする
読了目安 約3分
スロークエリを EXPLAIN で確かめ、インデックスを張り、速くなったことを確認する手順を型にします。
この章の目次
インデックスはなぜ速いのか の実践編です。 ISUCON のお題アプリは、必要なインデックスが意図的に抜かれていることが多く、「張るだけで数倍」がよく起きます。 勘で張るのではなく、計測から確認までを型にします。
手順の型
- スロークエリログの集計で、合計時間が上位のクエリを特定する
- そのクエリを
EXPLAINにかけ、全行走査になっていることを確かめる ALTER TABLE ... ADD INDEXでインデックスを張る- もう一度
EXPLAINにかけ、インデックスが使われたことを確かめる - ベンチを回してスコアで確かめる
EXPLAIN の読み方
EXPLAIN は、「MySQL がそのクエリをどう実行するつもりか」を表で返す命令です。
クエリの先頭に付けて使います。
まず見るのは type、rows、key の 3 列です。
前章の順位表で「600 回で 61 秒」だったクエリが、これだったとします。
SELECT * FROM posts WHERE user_id = 42 ORDER BY created_at DESC LIMIT 20;結果はこうです(数値は例です)。
mysql> EXPLAIN SELECT * FROM posts WHERE user_id = 42 ORDER BY created_at DESC LIMIT 20;
+----+-------------+-------+------+---------------+------+---------+------+--------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-----------------------------+
| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 985000 | Using where; Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+--------+-----------------------------+- type:行の探し方。
ALLは全行走査(テーブルの全行を読んで条件に合うものを探すやり方)で、最悪の値 - key:実際に使われたインデックス。
NULLは「使えるものがなかった」 - rows:読む見込みの行数。ここでは約 98 万行、つまりテーブル全体
- Extra の
Using filesort:ORDER BYのための並べ替えを、取り出した行に対して別途実行している印
20 件返すために 98 万行を読んで並べ替えているわけです。
複合インデックスを張る
このクエリは WHERE user_id = ? で絞り、created_at の降順で並べています。
複数の列を順に並べた複合インデックスに、両方の列を入れます。
ALTER TABLE posts ADD INDEX idx_user_id_created_at (user_id, created_at);列の順序が肝心で、等値で絞る user_id を先、並べ替えの created_at を後に置きます。
このインデックスでは user_id = 42 の区画に一気に飛べ、区画の中はすでに created_at 順なので並べ替えも不要になります。
張ったら、もう一度 EXPLAIN です。
+----+-------------+-------+------+------------------------+------------------------+---------+-------+------+---------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+------------------------+------------------------+---------+-------+------+---------------------+
| 1 | SIMPLE | posts | ref | idx_user_id_created_at | idx_user_id_created_at | 4 | const | 120 | Backward index scan |
+----+-------------+-------+------+------------------------+------------------------+---------+-------+------+---------------------+- type:
ALLからref(インデックスで特定の値に一致する行だけを読む探し方)に変わった - key:張ったインデックスの名前が入った
- rows:98 万から 120 に落ちた
- Extra:
Using filesortが消えた。Backward index scanは、昇順のインデックスを逆向きに読んで降順を作っている印
type が ref や range(範囲での絞り込み)になっていれば、インデックスは効いています。
確認できたらベンチを回し、順位表からこのクエリが落ちたことを見届けます。
張ったのに効かないとき
EXPLAIN の key が NULL のままなら、次の 2 つを疑ってください。
1 つ目は、列に関数を噛ませている場合です。
-- 効かない
SELECT * FROM posts WHERE DATE(created_at) = '2026-07-17';インデックスは列の生の値の順に並んでいるので、関数を通した後の値では引けません。 生の値のまま範囲条件に書き換えれば効きます。
-- 効く
SELECT * FROM posts
WHERE created_at >= '2026-07-17' AND created_at < '2026-07-18';2 つ目は、複合インデックスの左端ルールです。
(user_id, created_at) は、user_id を指定しない WHERE created_at > ? だけの検索には使えません。
姓、名の順で並んだ名簿から名だけで人を探せないのと同じで、左端の列(またはそこから連続する先頭部分)を条件に使うときだけ効きます。
インデックスは書き込みのたびに維持コストがかかるので、スロークエリの上位に出たクエリにだけ張ります。
次章は、クエリの回数そのものを減らす手、キャッシュです。