Day17 前半のゴール
「“なんとなく”ではなく“理由を持って”インデックスを貼れるようになる」
Day17 のテーマはインデックス設計です。
ここからが、パフォーマンスと設計の本丸です。
一言で言うと、今日やりたいことはこれです。
「どこにインデックスを貼るべきか」を、感覚ではなく“説明できる理由”で判断できるようになること
前半のゴールは、
インデックスが効く場面・効かない場面のイメージを持つ
「このカラムには貼るべき」「これはいらない」をざっくり仕分けできる
EXPLAIN と結びつけて考えられるようになる
ここまで行けば、後半で複合インデックスや細かい設計指針に入っていくときに、土台がかなり楽になります。
まず「インデックスの役割」を言い直す
「“検索を速くする”だけじゃなく“どの検索を速くするか”を選ぶ」
インデックスは、ざっくり言えば「検索を速くするためのデータ構造」です。
でも、ここで一歩踏み込んで言い直します。
インデックスは、「特定の検索パターンを速くするための専用の道」
つまり、
どの検索パターンを優先して速くしたいか
どの検索は多少遅くても許容するか
を決めるのが、インデックス設計です。
全部のカラムにインデックスを貼ればいい、という話ではありません。
インデックスにもコストがあります。
INSERT / UPDATE / DELETE のたびにインデックスも更新される
インデックス自体がディスク容量を食う
だからこそ、
「ここは絶対に速くしたい」
「ここはそこまでシビアじゃない」
という線引きを、あなたが決める必要があります。
インデックスが“効きやすい”典型パターン
「WHERE・JOIN・ORDER BY・GROUP BY によく出てくるカラム」
まずは、「ここはインデックス候補になりやすい」という典型パターンを押さえましょう。
WHERE でよく絞り込みに使うカラム
JOIN の ON 句でテーブル同士をつなぐカラム
ORDER BY で並び替えに使うカラム
GROUP BY で集計のキーになるカラム
この4つは、インデックス設計の“主戦場”です。
逆に言うと、
SELECT 句に出てくるだけのカラム
(ただ表示したいだけの項目)
には、基本的にインデックスは不要です。
インデックスは「どう探すか」に効くものであって、「何を表示するか」には直接関係しません。
例題テーブルで「貼る/貼らない」を考えてみる
「users テーブルを題材に“よくあるクエリ”から逆算する」
次のような users テーブルを考えます。
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at DATETIME NOT NULL
) ENGINE=InnoDB;
Pythonここに対して、アプリがよく投げるクエリを想像してみます。
メールアドレスでユーザーを1件探す
SELECT *
FROM users
WHERE email = 'foo@example.com';
Pythonアクティブユーザー一覧を新しい順に出す
SELECT *
FROM users
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 50;
Python特定期間に登録されたユーザー数を集計する
SELECT COUNT(*)
FROM users
WHERE created_at BETWEEN '2025-01-01' AND '2025-01-31';
Pythonこの3つを見ながら、「どこにインデックスを貼るべきか」を考えてみます。
パターン1:一意に探すカラム(email)
「“1件だけピンポイントで取る”検索はほぼ確実にインデックス候補」
最初のクエリです。
SELECT *
FROM users
WHERE email = 'foo@example.com';
Pythonメールアドレスは、通常はユーザーごとに一意です。
このクエリは、「email で1件だけピンポイントに探す」パターンです。
こういうカラムは、ほぼ確実にインデックス候補です。
インデックスなしだと、
users テーブルを全件スキャンして
1行ずつ email を見て
一致する行を探す
という動きになります。
インデックスがあれば、
インデックスの木構造をたどって
該当する email の位置を一気に特定し
対応する行だけを取る
という動きになります。
EXPLAIN で見ると、違いはこう出ます。
インデックスなし(悪い例)
| table | type | key | rows |
|---|---|---|---|
| users | ALL | NULL | 100000 |
インデックスあり(良い例)
| table | type | key | rows |
|---|---|---|---|
| users | ref | idx_email | 1 |
Day16 でやったように、
type = ALL / key = NULL / rows が大きい → フルスキャンで危ない
type = ref / key にインデックス名 / rows が小さい → インデックスが効いていて良い
という判断ができます。
結論として、
email のような「一意に探すことが多いカラム」には、インデックスを貼る価値が高い
と覚えておいてOKです。
パターン2:状態+並び順(status × created_at)
「“よく絞る条件”と“よく並べる条件”はセットで意識する」
次のクエリです。
SELECT *
FROM users
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 50;
Pythonここでは、
status で絞り込み
created_at で並び替え
をしています。
このとき、インデックス設計の考え方は2段階です。
まず、「status にインデックスを貼るべきか?」を考えます。
status の値の種類が少ない(例:’active’, ‘inactive’, ‘banned’ の3種類)場合、
インデックスの効果は限定的です。
なぜかというと、
status = ‘active’ の行が全体の 80% を占めるような場合、
インデックスを使っても結局ほとんどの行を読むことになる
からです。
一方、active ユーザーが全体の 5% くらいしかいないなら、
status インデックスはかなり効きます。
ここで大事なのは、
インデックスは「絞り込みが効く」カラムに貼ると効果が高い
という感覚です。
次に、「created_at での ORDER BY」をどうするか、です。
前半では深追いしませんが、チラ見せだけしておきます。
status と created_at を組み合わせた複合インデックス
CREATE INDEX idx_users_status_created
ON users (status, created_at);
Pythonを貼ると、
status で絞り込み
created_at で並び替え
の両方をインデックスでカバーできる可能性が出てきます。
EXPLAIN の Extra に「Using filesort」が出ているかどうかを見ながら、
インデックス設計を調整していく、という流れになります。
Day17 前半では、
「よく絞る条件」と「よく並べる条件」は、インデックス設計でセットで考える
という視点だけ持っておけば十分です。
パターン3:期間集計(created_at)
「“範囲でよく絞るカラム”もインデックス候補」
最後のクエリです。
SELECT COUNT(*)
FROM users
WHERE created_at BETWEEN '2025-01-01' AND '2025-01-31';
Pythonこれは、「特定期間に登録されたユーザー数」を数えるクエリです。
created_at にインデックスがない場合、
MySQL は users を全件スキャンして、
1行ずつ created_at を見て条件に合うか判定します。
created_at にインデックスがある場合、
インデックスの範囲検索で
‘2025-01-01’ 〜 ‘2025-01-31’ に該当する部分だけを読み
という動きになります。
EXPLAIN で見ると、こういう違いになります。
インデックスなし
| table | type | key | rows |
|---|---|---|---|
| users | ALL | NULL | 100000 |
インデックスあり
| table | type | key | rows |
|---|---|---|---|
| users | range | idx_created_at | 5000 |
type = range は、「インデックスを使った範囲検索」を意味します。
rows も 100000 → 5000 のように、読む行数が減っています。
期間集計のように、
特定の範囲だけをよく見るカラム
も、インデックス候補としてかなり有力です。
「貼るべきか?」を判断するための3つの問い
「クエリから逆算して“優先度の高いカラム”を見つける」
ここまでの例をまとめると、
インデックスを貼るかどうかを判断するときに、自分に投げるべき問いはだいたいこうなります。
このカラムは、WHERE や JOIN でよく使われるか?
使われるなら、「どれくらい絞り込みが効くか?」(一意に近いか、値の種類が多いか)
このカラムは、ORDER BY や GROUP BY でよく使われるか?
使われるなら、「そのクエリは重くなりがちか?」(大量行を並び替え・集計していないか)
このテーブルに対する“よくあるクエリ”を10個挙げたとき、
その中で何回も登場するカラムはどれか?
インデックス設計は、「テーブルを見て考える」というより、
そのテーブルに対して、どんなクエリが飛んでくるか
から逆算するのが基本です。
Day17 前半では、
インデックスは“クエリのために貼るもの”であって、“テーブルのために貼るもの”ではない
という感覚を、しっかり持っておいてほしいです。
Day17 前半のまとめ
インデックスは「検索を速くするための専用の道」なので、どこに貼るかを決めるときはテーブル構造そのものではなく「そのテーブルに対してどんなクエリが飛んでくるか」から逆算し、特に WHERE・JOIN・ORDER BY・GROUP BY に頻出するカラムが主な候補になる。
users テーブルの例で言えば、email のように「1件をピンポイントで探す」カラムはほぼ確実にインデックス候補で、EXPLAIN したときに type = ALL / key = NULL / rows が大きい 状態から、type = ref / key にインデックス名 / rows が小さい 状態に変わることで効果を確認でき、created_at のように「範囲でよく絞る」カラムも type = range で行数を大きく減らせるため有力な候補になる。
一方で、status のように値の種類が少なく「絞り込みがあまり効かない」カラムは、インデックスを貼っても効果が薄い場合があり、「よく絞る条件」と「よく並べる条件(ORDER BY)」をセットで考えたり、「よく使うクエリを10個挙げたときに何度も登場するカラムはどれか?」と自分に問いかけることで、「ここは絶対に速くしたい」「ここはそこまでシビアじゃない」という優先度をつけてインデックス設計をしていくのが、実務的な第一歩になる。
後半では、
複合インデックス(カラムを並べて貼る)の考え方、
インデックスが“効かなくなる”書き方(関数をかける・前方一致でない LIKE など)、
INSERT/UPDATE とのトレードオフをどう考えるか、
を具体的な EXPLAIN の結果とセットで深掘りしていきます。
