いちばん基本の結合
注文は customer_id を持ち、顧客は名前を持っています。一緒に見るには、2つのテーブルがどう対応するかをデータベースに伝えます。
1
SELECT o.order_number,
o.ordered_at,
o.total,
c.first_name || ' ' || c.last_name AS customer,
c.country
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY o.ordered_at DESC
LIMIT 20;
結果 — 20 行のうち先頭 5 行
| order_number | ordered_at | total | customer | country |
|---|---|---|---|---|
| ORD-2025-002480 | 2025-12-28 23:52:14 | 6333.77 | Elena Meyer | CA |
| ORD-2025-000505 | 2025-12-28 11:16:39 | 2276.51 | Diego Rossi | IT |
| ORD-2025-000940 | 2025-12-28 07:28:46 | 11000.68 | Freya Choi | BR |
| ORD-2025-001581 | 2025-12-28 06:12:38 | 2645.06 | Jonas Lee | JP |
| ORD-2025-000995 | 2025-12-28 05:47:53 | 2584.02 | Leo Haddad | US |
ブラウザの別タブで Planche が開き、サンプルデータベースを読み込んだ状態でこのクエリが入ります。このページの例はどれも同じタブを使い回します。F9 を押すまで実行はされません。
名前を付けておくべきものが3つあります。JOIN だけ書けば INNER JOIN の意味です。ON c.id = o.customer_id は結合条件で、左の行と右の行をどう対応させるかを決めます。そして orders o はテーブルに短い別名を付けていて、これがあるから毎回テーブル名を書かずに o.total や c.country と書けます。
2つのテーブルが同じ名前の列を持った瞬間から、別名は「あってもよいもの」ではなくなります。この2つのテーブルはどちらも id を持っているので、修飾しない id は曖昧で、データベースはクエリを拒否します。
3つ以上のテーブル
結合はつなげられます。注文の商品は2ステップ先です。orders → order_items → products。
2
SELECT o.order_number,
p.name AS product,
oi.quantity,
oi.unit_price
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'delivered'
ORDER BY o.ordered_at DESC
LIMIT 25;
結果 — 25 行のうち先頭 5 行
| order_number | product | quantity | unit_price |
|---|---|---|---|
| ORD-2025-000505 | Zen Boots | 1 | 700.65 |
| ORD-2025-000505 | Echo Backpack 40L Plus | 1 | 1570.96 |
| ORD-2025-001581 | Atlas Phone Pro Max | 1 | 1546.38 |
| ORD-2025-001581 | Pulse Headphones | 2 | 548.09 |
| ORD-2025-000995 | Aria Chrono | 2 | 1289.56 |
JOIN 1つでテーブルが1つ増えるだけなので、4テーブルの結合はこれに1行足しただけです。上から順に「注文と、その明細と、明細ごとの商品」と読めます。
一致しない行も残す
内部結合は相手がいない行を落とします。一度も注文していない顧客は結果から消えます。LEFT JOIN は左のテーブルの行を全部残し、対応するものがなければ右側を NULL で埋めます。
3
SELECT c.id,
c.first_name,
c.last_name,
o.order_number,
o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.id
LIMIT 25;
結果 — 25 行のうち先頭 5 行
| id | first_name | last_name | order_number | total |
|---|---|---|---|---|
| 1 | Liam | Fischer | ORD-2022-001596 | 1317.56 |
| 1 | Liam | Fischer | ORD-2025-001723 | 2664.56 |
| 1 | Liam | Fischer | ORD-2025-003142 | 4609.77 |
| 2 | Mateo | Costa | ORD-2025-002473 | 3760.6 |
| 3 | Yuki | Jang | ORD-2025-003527 | 7569.3 |
これで「ないものを探す」クエリが書けます。全部残してから、一致しなかった行だけを残すのです。
4
-- The 95 customers who have never ordered anything.
SELECT c.id, c.first_name, c.last_name, c.country
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL
ORDER BY c.id
LIMIT 25;
結果 — 25 行のうち先頭 5 行
| id | first_name | last_name | country |
|---|---|---|---|
| 14 | Maya | Moreau | US |
| 17 | Nora | Müller | IT |
| 20 | Sana | Park | KR |
| 30 | Ravi | Park | KR |
| 33 | Oliver | Park | IN |
WHERE o.id IS NULL は最初は妙に見えます。orders.id は主キーで、テーブルの中では決して NULL になりません。でもこの結果の中では NULL になれます。一致しなかった行のために LEFT JOIN がその NULL を作ったからです。その行が、一度も注文していない95人の顧客です。
ON か WHERE か:同じ質問ではない
ここが罠です。次の2つのクエリはどちらも status = 'cancelled' と書いています。実際に LEFT JOIN として働くのは片方だけです。
5
-- The condition is part of the join: every customer is kept,
-- and only cancelled orders are attached to them.
SELECT c.id, c.first_name, o.order_number, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'cancelled'
ORDER BY c.id
LIMIT 25;
結果 — 25 行のうち先頭 5 行
| id | first_name | order_number | status |
|---|---|---|---|
| 1 | Liam | NULL | NULL |
| 2 | Mateo | NULL | NULL |
| 3 | Yuki | ORD-2025-003530 | cancelled |
| 4 | Ines | NULL | NULL |
| 5 | Leo | NULL | NULL |
条件が ON にあるので、これは対応付けの一部です。顧客は全員残り、キャンセルされた注文がある人にはそれが付きます。キャンセル注文がない顧客も NULL 付きで現れます。
6
-- The same condition in WHERE runs after the join and throws away
-- every row where it is not true — including the NULLs the LEFT JOIN made.
SELECT c.id, c.first_name, o.order_number, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'cancelled'
ORDER BY c.id
LIMIT 25;
結果 — 25 行のうち先頭 5 行
| id | first_name | order_number | status |
|---|---|---|---|
| 3 | Yuki | ORD-2025-003530 | cancelled |
| 6 | Nora | ORD-2023-001551 | cancelled |
| 6 | Nora | ORD-2022-002224 | cancelled |
| 7 | Priya | ORD-2022-003262 | cancelled |
| 10 | Elena | ORD-2025-000182 | cancelled |
こちらの条件は WHERE にあるため、結合後に評価されます。LEFT JOIN が作った NULL の行も条件を満たす必要がありますが、NULL = 'cancelled' は真になりません。その行は除外され、結果は INNER JOIN と同じになります。
LEFT JOIN の右側のテーブルを絞る条件は ON に、左側のテーブルを絞る条件は WHERE に書く、と考えると整理しやすくなります。INNER JOIN では両者が同じ結果になるため、この違いを見落としがちです。
同じテーブルを自分自身と結合する
両側が違うテーブルでなければならない決まりはありません。従業員は別の従業員を指す manager_id を持っているので、テーブルは2つの別名の下で自分自身と結合できます。
7
SELECT e.first_name || ' ' || e.last_name AS employee,
e.position,
m.first_name || ' ' || m.last_name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id
ORDER BY manager, employee
LIMIT 25;
結果 — 25 行のうち先頭 5 行
| employee | position | manager |
|---|---|---|
| Ava Müller | Director | NULL |
| Hana Tanaka | Director | NULL |
| Hugo Ahmed | Director | NULL |
| Lucas Romero | Director | NULL |
| Maya Müller | Director | NULL |
e と m は同じテーブルを2回見ているだけで、成立させているのは別名です。LEFT JOIN にしてあるのは、上層の10人には上司がいないからです。内部結合ではその10人が黙って消えます。
ここまでで書けるもの
スキーマ全体をまたいだ質問ができるようになりました。まだできないのは、その答えを要約することです。注文を数える、売上を合計する、平均を出す。それが次のレッスンです。