はじめに――mysql left join は「結合対象の右テーブルに一致する行がなくても左テーブルの行を残す」機能です。この記事では構文から実戦的な落とし穴、性能チューニングまで 初学者~中級者が知りたいポイント を 1 ページでまとめます。
この記事で得られること
- LEFT JOIN の正しい書き方と動く実例
- NULL 行が消える・意図しないフィルタリングなど典型的トラブルの回避策
EXPLAIN
とインデックスで 10× 速くする 5 箇条- Laravel クエリビルダとの橋渡し情報
LEFT JOIN とは何か
一文で言うと:「左テーブルを主とし、右テーブルにヒットしなければ
NULL
を補完して返す結合」。
- 用途: マスタ付き一覧、ログ+ユーザ情報、月次集計の “欠損行” 保持など
- INNER JOIN との違い: INNER は両テーブルに一致する行のみ返すため “欠損” が落ちる点が決定的
LEFT / RIGHT / FULL の位置関係
種類 | 優先テーブル | 不一致行の扱い |
---|---|---|
LEFT JOIN | 左 (FROM 句側) | NULL 行を残す |
RIGHT JOIN | 右 | NULL 行を残す |
FULL JOIN* | 両方 | 両方に残す |
注:MySQL では FULL JOIN
を直接サポートしていないため UNION
で代用します。
基本構文と実例
最小構文は下記のとおりです(ON
句が必須)。
SELECT
u.id, u.name, p.phone
FROM users AS u
LEFT JOIN phones AS p
ON p.user_id = u.id;
実例 1:欠損を残すマスタ付き一覧
SELECT
m.month,
COALESCE(s.amount, 0) AS amount
FROM months AS m
LEFT JOIN sales AS s
ON s.month = m.month
ORDER BY m.month;
COALESCE()
でNULL
を 0 へ変換し「集計抜け」を防ぎます。- Laravel なら
leftJoin
→selectRaw('COALESCE(s.amount,0) as amount')
で同等。
よくある落とし穴と対処法
WHERE
句に右テーブルの列を書いてしまう
WHERE p.phone IS NOT NULL
は LEFT JOIN を実質 INNER JOIN に変えてしまいます。- ✅
ON
句またはHAVING
句に条件を書く。
GROUP BY
で NULL 行が消える
- 集約前に
COALESCE()
で置き換えるか、ダミー行を UNION で足す。
複合キー JOIN の一部が NULL
ON (a.x = b.x OR (a.x IS NULL AND b.x IS NULL))
のような冗長判定より、
結合キーを単一 NOT NULL 列に正規化する方が高速。
多対多の LEFT JOIN で倍増行
DISTINCT
やサブクエリで重複を削る。理想はEXISTS
で書き換え。
NULL と空文字の混在
- 文字列型の JOIN キーなら
TRIM()
とNULLIF()
で事前正規化。
EXPLAIN で実行計画を読む
クエリチューニングの第一歩は EXPLAIN
。結合順序 と 使用インデックス を確認しましょう。
EXPLAIN FORMAT=TRADITIONAL
SELECT u.id, p.phone
FROM users AS u
LEFT JOIN phones AS p
ON p.user_id = u.id
type
列がALL
なら全表走査。ref
やeq_ref
を目指す。possible_keys
にインデックス候補が出ていなければ、結合キーに(user_id)
を追加。- 8.0.24+ は
EXPLAIN ANALYZE
で実行時間も取得可。
パフォーマンス最適化 5 箇条
- 結合キーは必ずインデックス(FK 列 + 参照側 PK の複合などは要注意)
- JOIN キーに関数をかけない(
DATE(created_at)
は非索引化) - 取得列を絞りカバリングインデックス(
SELECT *
を捨てる) - 大規模集計は WITH (CTE) + 一時テーブルで分割
- EXPLAIN → インデックスヒント で強制指定も最終手段として活用
-
LEFT JOIN
とINNER JOIN
のどっちを選べばいい? -
- 欠損行を残して集計したい → LEFT JOIN
- 両テーブルに揃った行だけで OK → INNER JOIN
-
WHERE
に右テーブルの列を書いたら行が減ったんだけど? -
LEFT JOIN の意味が消えます。
WHERE phones.phone IS NOT NULL
のような条件は、ON
句かHAVING
句へ移動しましょう。
-
GROUP BY
後に NULL 行が消えるのを防ぐ方法は? -
集計の前に
COALESCE()
で NULL → 0 などに変換するか、先にサブクエリで集計してから LEFT JOIN します。
-
結合キーに関数 (
DATE(created_at)
など) を書いてもいい? -
速度が激落ちします。インデックスが効かなくなるため
- 生の列で結合
- 必要なら表示側で
DATE_FORMAT()
などを当てる
が鉄則です。
-
多対多の LEFT JOIN で行が倍増して困る!
-
中間テーブルをサブクエリで一意にまとめてから JOIN するか、
DISTINCT
/EXISTS
を検討してください。根本はテーブル設計の見直しが近道です。
-
LEFT JOIN
が遅いとき何から疑えばいい? -
- 結合キーに 単体インデックス があるか
- 取得列を絞って カバリングインデックス を作れるか
EXPLAIN
でtype = ALL
(全表走査)になっていないか
この 3 点を順にチェックするだけで大半のボトルネックは解消します。
-
FULL JOIN
がないけど両方の欠損行を取りたい! -
LEFT と RIGHT を
UNION ALL
で重ね、重複行をDISTINCT
で消すのが定番です。実装コストが高いならアプリ層でマージする手もあり。
コメントを残す