SQLのJOINを図なしで理解する話

SQLの JOIN って、だいたいベン図で説明されますよね。INNER JOIN は重なった部分、LEFT JOIN は左の円まるごと、みたいな。

あの図、最初はわかった気になるんですが、実際にクエリを書く段階で「で、どう書くんだっけ」ってなりがちです。少なくとも僕はそうでした。図の理解とクエリの理解がつながらない。

というわけで、ベン図を一切使わずに JOIN を整理してみます。テーブルの具体例だけで説明するメモです。

まず前提:JOINは「横にくっつける」

ざっくり言うと、JOIN は2つのテーブルを横に並べる操作です。UNION が縦に積むのに対して、JOIN は横。これだけ覚えておけばたぶん大丈夫です。

例として、こんな2つのテーブルを使います。

-- users
| id | name   |
|----|--------|
| 1  | 田中   |
| 2  | 鈴木   |
| 3  | 佐藤   |

-- orders
| id | user_id | item     |
|----|---------|----------|
| 1  | 1       | りんご   |
| 2  | 1       | バナナ   |
| 3  | 2       | みかん   |
| 4  | 99      | ぶどう   |

佐藤さん(id=3)は注文がない。ordersuser_id=99users に存在しない。このズレが JOIN の種類で結果を変えるポイントです。

INNER JOIN:両方にあるやつだけ

SELECT users.name, orders.item
FROM users
INNER JOIN orders ON users.id = orders.user_id;

結果:

| name | item     |
|------|----------|
| 田中 | りんご   |
| 田中 | バナナ   |
| 鈴木 | みかん   |

usersorders の両方に対応するデータがある行だけ出ます。佐藤さんは注文がないので消える。user_id=99 のぶどうも users に該当がないので消える。

平たく言うと「マッチしたものだけ出す」です。

ここでハマりやすいポイント
田中さんが2行出ていますよね。JOIN は条件に合う組み合わせを全部出すので、注文が2件あれば2行になります。「あれ、行数増えてる」ってなったらだいたいこれです。

LEFT JOIN:左のテーブルは全部出す

SELECT users.name, orders.item
FROM users
LEFT JOIN orders ON users.id = orders.user_id;

結果:

| name | item     |
|------|----------|
| 田中 | りんご   |
| 田中 | バナナ   |
| 鈴木 | みかん   |
| 佐藤 | NULL     |

FROM に書いた users(左側)は全行出ます。マッチする注文がない佐藤さんも出る。ただし orders 側のカラムは NULL になります。

user_id=99 のぶどうは出ません。あくまで左のテーブルが基準。

自分がよく使うのはこの LEFT JOIN です。「ユーザー一覧を出しつつ、注文があれば注文情報もつけたい」みたいなケース。実務だとこのパターンが一番多い気がします。

RIGHT JOIN:右のテーブルが全部出る

SELECT users.name, orders.item
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;

結果:

| name | item     |
|------|----------|
| 田中 | りんご   |
| 田中 | バナナ   |
| 鈴木 | みかん   |
| NULL | ぶどう   |

今度は orders(右側)が全行出ます。user_id=99 のぶどうも出るけど、users.nameNULL。佐藤さんは消えます。

正直、RIGHT JOIN はほとんど使ったことがないです。LEFT JOIN でテーブルの順番を入れ替えれば同じ結果が出せるので。実際に RIGHT JOIN をコードレビューで見たときも「LEFT JOIN に書き直したほうが読みやすくない?」ってコメントした記憶があります。

FULL OUTER JOIN:両方全部出す

SELECT users.name, orders.item
FROM users
FULL OUTER JOIN orders ON users.id = orders.user_id;

結果:

| name | item     |
|------|----------|
| 田中 | りんご   |
| 田中 | バナナ   |
| 鈴木 | みかん   |
| 佐藤 | NULL     |
| NULL | ぶどう   |

両方のテーブルの全行が出ます。マッチしなかった側は NULL

注意: MySQLは FULL OUTER JOIN をサポートしていません。最初これ知らなくて、なんでエラーになるんだろうと焦りました。

MySQLでやりたい場合は LEFT JOINRIGHT JOINUNION で合体させます。

SELECT users.name, orders.item
FROM users LEFT JOIN orders ON users.id = orders.user_id
UNION
SELECT users.name, orders.item
FROM users RIGHT JOIN orders ON users.id = orders.user_id;

めんどくさいですが、しょうがない。

CROSS JOIN:全組み合わせ

SELECT users.name, orders.item
FROM users
CROSS JOIN orders;

結果は users 3行 × orders 4行 = 12行。全部の組み合わせが出ます。ON 句がないのが特徴。

使い道としては、カレンダーの全日付とカテゴリの組み合わせを作るとか、集計用のマスタを生成するとかですかね。日常的にはあまり使わないです。うっかり ON を書き忘れてこれになって、「なんか結果が爆発してるんだけど」ってなった経験があります。

個人的によくやるミス

NULLの比較

LEFT JOIN した結果を WHERE で絞るとき、NULL の扱いでやらかしがちです。

-- これはダメ
SELECT * FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.item = NULL;

-- こう書く
WHERE orders.item IS NULL;

= NULL は常に FALSE になるので、何も返ってきません。SQLにおける NULL は「不明」なので、= で比較できない。頭ではわかっているのに、急いでいると書いちゃうんですよね。

JOINの順番

3つ以上のテーブルを JOIN するとき、順番を間違えると結果がおかしくなることがあります。

-- usersを起点にしたいのに
SELECT *
FROM orders
LEFT JOIN users ON users.id = orders.user_id
LEFT JOIN addresses ON users.id = addresses.user_id;

これだと orders が基準になるので、注文がないユーザーが消えます。起点にしたいテーブルを FROM に書く。当たり前なんですが、テーブルが増えてくると見失いがちです。

所感

JOIN の説明ってベン図がセットになりがちだけど、実際のテーブルを並べたほうがわかりやすいんじゃないかとずっと思っていました。「この条件でくっつけたら、どの行が残って、どこが NULL になるか」が見えれば、たぶんそれで十分です。

あと、実務だと INNER JOINLEFT JOIN でほぼ事足ります。RIGHT JOINFULL OUTER JOIN は知識としては知っておくけど、書くことはほとんどない。CROSS JOIN は存在を知っておけば、うっかり ON を書き忘れたときに「あーこれか」とすぐ気づけます。

ここまで読んでいただき、ありがとうございます。もしこの記事の技術や考え方に少しでも興味を持っていただけたら、ネクストのエンジニアと気軽に話してみませんか。

  • 選考ではありません
  • 履歴書不要
  • 技術の話が中心
  • 所要時間30分程度
  • オンラインOK

エンジニアと話してみる

関連リンク

AI・クラウド・データ分析のご相談はネクスト株式会社までお問い合わせください。