JOIN
2 つ以上のテーブルを関連列で結合する。 INNER / LEFT / RIGHT / FULL / CROSS / SELF / LATERAL を一通り。
INNER JOIN(内部結合)
両側に該当行がある組み合わせのみ返す。最も一般的。
SELECT u.email, p.title
FROM users u
INNER JOIN posts p ON p.user_id = u.id;
-- INNER は省略可
SELECT u.email, p.title
FROM users u JOIN posts p ON p.user_id = u.id;
視覚的なイメージ
users posts
+----+------+ +----+--------+
| 1 | A | | 1 | post1 | user_id=1
| 2 | B | | 2 | post2 | user_id=1
| 3 | C | | 3 | post3 | user_id=2
+----+------+ | 4 | post4 | user_id=99 ← 親なし
+----+--------+
INNER JOIN 結果(user_id=99 は除外、user 3 は除外)
+------+--------+
| A | post1 |
| A | post2 |
| B | post3 |
+------+--------+
LEFT OUTER JOIN(左外部結合)
左テーブルの行は必ず残す。右に該当が無ければ NULL。
-- 「全ユーザ + 投稿があれば持ってくる」
SELECT u.email, p.title
FROM users u
LEFT JOIN posts p ON p.user_id = u.id;
LEFT JOIN 結果
+------+--------+
| A | post1 |
| A | post2 |
| B | post3 |
| C | NULL | ← 投稿無しでも残る
+------+--------+
「持っていない」検索
-- 「投稿が無いユーザ」
SELECT u.id, u.email
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE p.id IS NULL;
RIGHT OUTER JOIN(右外部結合)
LEFT の左右逆。実用上はLEFT に書き直すのが定番(読みやすいから)。
FULL OUTER JOIN
両側残す。どちらかに該当が無ければ NULL。
SELECT u.email, p.title
FROM users u
FULL OUTER JOIN posts p ON p.user_id = u.id;
差分検出
-- どちらか片方にしか無い行
SELECT
COALESCE(a.id, b.id) AS id,
CASE WHEN a.id IS NULL THEN 'B のみ'
WHEN b.id IS NULL THEN 'A のみ' END AS where_
FROM table_a a
FULL JOIN table_b b ON a.id = b.id
WHERE a.id IS NULL OR b.id IS NULL;
CROSS JOIN(直積)
条件無しで全組み合わせ。非常に重いので意図して使う。
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;
-- sizes 3 × colors 5 = 15 行
SELF JOIN(自己結合)
同じテーブルを 2 回参照。階層構造などで使う。
-- comments テーブルが parent_id で階層を持つ
SELECT
child.id, child.body,
parent.body AS parent_body
FROM comments child
LEFT JOIN comments parent ON parent.id = child.parent_id;
USING / NATURAL JOIN
-- 同名列で結合
SELECT * FROM users u JOIN posts p USING (user_id);
-- 全同名列で自動結合(事故りやすい、推奨されない)
SELECT * FROM users NATURAL JOIN posts;
3 つ以上のテーブル
SELECT
u.email,
p.title,
c.body AS comment_body
FROM users u
JOIN posts p ON p.user_id = u.id
JOIN comments c ON c.post_id = p.id
WHERE u.email = 'alice@example.com';
ON と WHERE の違い(OUTER JOIN で重要)
- ON: 結合条件。OUTER JOIN では NULL 行も生成
- WHERE: 結合後のフィルタ。NULL 行も削れる
-- 「公開中の投稿があれば、ユーザと一緒に」
SELECT u.id, p.title
FROM users u
LEFT JOIN posts p ON p.user_id = u.id AND p.status = 'published';
-- ↑ ON 句に条件 → 投稿無しユーザも残る
-- ↓ WHERE に書くと INNER と同じ結果に
SELECT u.id, p.title
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE p.status = 'published';
LATERAL JOIN(Postgres)
外側のテーブルの列を内側のサブクエリで使える。各行ごとにサブクエリを評価。
-- 各ユーザの最新 5 投稿
SELECT u.id, u.email, p.*
FROM users u
LEFT JOIN LATERAL (
SELECT * FROM posts
WHERE user_id = u.id
ORDER BY created_at DESC
LIMIT 5
) p ON true;
Window 関数の ROW_NUMBER() OVER (PARTITION BY ...) でも同じことができる。
LATERAL は SQL 的に直感的、Window は性能が良いケースが多い。
JOIN の典型クエリ
1. ユーザの投稿数
SELECT u.id, u.email, count(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id;
2. 投稿 + 著者 + コメント数
SELECT
p.id, p.title,
u.name AS author,
count(c.id) AS comment_count
FROM posts p
JOIN users u ON u.id = p.user_id
LEFT JOIN comments c ON c.post_id = p.id
GROUP BY p.id, u.name;
3. ユーザのフォロワー(中間テーブル)
-- follows(follower_id, followee_id)
SELECT followee.email AS being_followed, follower.email AS follower
FROM follows f
JOIN users follower ON follower.id = f.follower_id
JOIN users followee ON followee.id = f.followee_id
WHERE follower.id = 42;
4. 友達 of 友達
SELECT DISTINCT fof.id, fof.email
FROM follows f1
JOIN follows f2 ON f1.followee_id = f2.follower_id
JOIN users fof ON fof.id = f2.followee_id
WHERE f1.follower_id = 42
AND fof.id != 42;
パフォーマンス
- 結合列にインデックスを必ず(FK 列は基本貼っておく)
- JOIN の順序はプランナが決める(普通は触らない)
- 巨大テーブル × 巨大テーブルはWHERE で先に絞る
EXPLAIN ANALYZEで実際のプランを確認
JOIN の代わりに JSON 集約(Postgres)
-- 各投稿に「コメント配列」を埋め込む
SELECT
p.id, p.title,
json_agg(json_build_object('id', c.id, 'body', c.body)) AS comments
FROM posts p
LEFT JOIN comments c ON c.post_id = p.id
GROUP BY p.id;
REST / GraphQL レスポンスを 1 クエリで作れる強力な技。
失敗パターン
| 症状 | 原因 |
|---|---|
| 結果が増えすぎる | 1-N JOIN で重複が拡大。集計 / DISTINCT |
| 遅い | FK 列にインデックスなし |
| OUTER JOIN で行が消える | WHERE に条件を書いた → ON 句へ |
| SELF JOIN で混乱 | 必ず別エイリアス(child / parent) |
| 3 つ以上で迷子 | CTE で段階分解 |
早く慣れるコツ
紙に図を描いて、INNER と LEFT で残る行を可視化する。 これに 1 時間費やすと、後の生産性が桁違いに上がる。