MySQL | SQLite経験者向け、30日で習得するMySQL:パフォーマンスと設計 - Day17 インデックス設計

SQL MySQL
スポンサーリンク

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 で見ると、違いはこう出ます。

インデックスなし(悪い例)

tabletypekeyrows
usersALLNULL100000

インデックスあり(良い例)

tabletypekeyrows
usersrefidx_email1

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 で見ると、こういう違いになります。

インデックスなし

tabletypekeyrows
usersALLNULL100000

インデックスあり

tabletypekeyrows
usersrangeidx_created_at5000

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 の結果とセットで深掘りしていきます。

タイトルとURLをコピーしました