Day19 後半のゴール
「JOIN を“速くするための具体的な技術”を、自分で選んで使えるようになる」
前半では、JOIN が遅くなる理由と、EXPLAIN を使った原因特定の方法を整理しました。
後半では、いよいよ「どう改善するか」を具体的に掘り下げます。
ここでのゴールは次の状態です。
JOIN を速くするためのインデックス設計を説明できる
JOIN の順番がパフォーマンスにどう影響するか理解できる
JOIN を書き換えて速くする“実務的テクニック”を使える
JOIN は SQL の中でも最もパフォーマンス差が出る部分なので、
ここを押さえると実務レベルのチューニング力が一気に上がります。
JOIN を速くするためのインデックス設計
「JOIN の ON 句に出てくるカラムは“最優先で貼る”」
JOIN の改善で最も効果が大きいのは、やはりインデックスです。
ただし、貼る場所を間違えると効果が出ません。
JOIN の ON 句に出てくるカラムは、最優先でインデックス候補になります。
例として、前半で扱ったクエリを再掲します。
SELECT
o.id,
o.created_at,
p.name,
oi.quantity
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
JOIN users u ON o.user_id = u.id
WHERE u.id = 123
ORDER BY o.created_at DESC;
Pythonこの JOIN を速くするために必要なインデックスは、次のように整理できます。
orders.user_id
order_items.order_id
order_items.product_id
products.id(主キーなのでOK)
users.id(主キーなのでOK)
ここで重要なのは、
JOIN の片側だけにインデックスがあっても不十分なことがある
という点です。
例:order_items.order_id にインデックスがないとどうなるか
「orders の1行ごとに order_items を全件スキャンする地獄」
orders.id に主キーがあるのは当然ですが、
order_items.order_id にインデックスがないと、JOIN はこう動きます。
orders の1行を読む
order_items を全件スキャンして、order_id が一致する行を探す
次の orders の行でも同じことを繰り返す
つまり、orders の行数 × order_items の行数 という掛け算が発生します。
order_items.order_id にインデックスを貼るだけで、
「order_id = ○○ の行だけを一気に取る」動きに変わり、
JOIN のコストが劇的に下がります。
JOIN の改善で最初にやるべきことは、
JOIN の両側にインデックスがあるか確認する
ということです。
JOIN の順番がパフォーマンスに与える影響
「“どのテーブルから読むか”で rows の桁が変わる」
MySQL は JOIN の順番を自動で最適化しますが、
その判断はインデックスや統計情報に依存します。
EXPLAIN の id / table の順番を見ると、
どのテーブルから読み始めているかが分かります。
例えば、次のような EXPLAIN が出たとします。
| id | table | type | rows |
|---|---|---|---|
| 1 | users | const | 1 |
| 1 | orders | ref | 500 |
| 1 | order_items | ref | 2000 |
| 1 | products | ALL | 10000 |
この場合、
users → orders → order_items → products
の順で読み進めています。
ここで問題なのは products が ALL(フルスキャン)になっていることです。
JOIN の順番が悪いというより、products.product_id にインデックスがないのが原因です。
JOIN の順番をいじるよりも、
JOIN の順番が“自然に良くなるように”インデックスを整える
というのが正しいアプローチです。
JOIN の順番を変えると速くなるケース
「WHERE で絞れるテーブルを先に読ませたい」
例えば、次のようなクエリを考えます。
SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';
Pythonusers.status にインデックスがある場合、
MySQL は users を先に読む方が効率的です。
しかし、orders.user_id にしかインデックスがない場合、
MySQL は orders を先に読む可能性があります。
このように、
どのテーブルを先に読むべきかは、インデックス次第で変わる
ということを理解しておくと、
JOIN の順番に悩む前に「貼るべきインデックス」が見えてきます。
JOIN を書き換えて速くするテクニック
「JOIN の形を変えるだけで速くなることがある」
JOIN は書き方によってもパフォーマンスが変わります。
INNER JOIN にできるなら INNER JOIN にする
「LEFT JOIN は“残す側”が多いほど重くなる」
例えば、こういうクエリがあります。
SELECT *
FROM orders o
LEFT JOIN users u ON o.user_id = u.id;
Pythonしかし、実際には「ユーザーが存在しない注文はない」前提なら、
LEFT JOIN ではなく INNER JOIN で十分です。
SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id;
PythonLEFT JOIN は「結合できない行も残す」ため、
内部処理が増え、最適化も制限されます。
INNER JOIN にできるなら、積極的に変えるべきです。
サブクエリを JOIN に書き換える
「IN (SELECT …) は JOIN より遅くなりやすい」
前半でも触れましたが、次のような書き方は JOIN に書き換えるべきです。
SELECT *
FROM orders
WHERE user_id IN (
SELECT id FROM users WHERE status = 'active'
);
PythonJOIN にすると、インデックスが効きやすくなります。
SELECT o.*
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';
PythonJOIN するテーブルを減らす
「非正規化やキャッシュテーブルでJOINを“そもそも不要にする”」
JOIN が多すぎる場合、
非正規化や集計テーブルを作って JOIN 自体を減らすのも有効です。
例えば、
注文履歴画面で毎回 users と products を JOIN している
→ orders に user_name をコピーする
→ order_items に product_name をコピーする
こうすることで、JOIN を減らし、
クエリをシンプルにできます。
JOIN の改善は、
「JOIN を速くする」だけでなく「JOIN を減らす」方向もある
ということを覚えておいてください。
JOIN の改善を“手順化”する
「この順番で見れば、迷わず改善できる」
JOIN の改善は、次の順番で考えると迷いません。
まず、EXPLAIN でどのテーブルが重いか特定する
次に、JOIN の ON 句に出てくるカラムにインデックスを貼る
WHERE で絞れるテーブルを先に読ませるようにインデックスを整える
LEFT JOIN を INNER JOIN にできないか確認する
サブクエリを JOIN に書き換える
それでも遅いなら、JOIN を減らす(非正規化・集計テーブル)
この順番は、実務でそのまま使える“JOIN 改善の型”です。
Day19 後半のまとめ
JOIN の改善は「インデックス設計」「JOIN 順の理解」「書き方の工夫」の三本柱で成り立ち、特に ON 句に出てくるカラムにインデックスを貼るだけで orders × order_items のような巨大テーブル同士の JOIN が劇的に速くなる。
JOIN の順番は MySQL が自動で決めるが、EXPLAIN の id / table を見れば「どのテーブルから読み始めているか」が分かり、WHERE で強く絞れるテーブルにインデックスを貼ることで“自然に良い順番”を選ばせることができる。
さらに、LEFT JOIN を INNER JOIN に変える、IN (SELECT …) を JOIN に書き換える、JOIN を減らすために非正規化や集計テーブルを導入するなど、書き方の工夫でも大きく改善できるため、「EXPLAIN → インデックス → JOIN の書き方 → JOIN の削減」という順番で改善していくことが、実務で迷わない JOIN チューニングの基本になる。
