テーブルを組み合わせる:JOIN

顧客、注文、商品は通常は別々のテーブルに保存します。JOIN はキーをたどって、関係する行を一度の検索で読み出す方法です。

いちばん基本の結合

注文は 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;
Planche で実行

結果 — 20 行のうち先頭 5 行

order_numberordered_attotalcustomercountry
ORD-2025-0024802025-12-28 23:52:146333.77Elena MeyerCA
ORD-2025-0005052025-12-28 11:16:392276.51Diego RossiIT
ORD-2025-0009402025-12-28 07:28:4611000.68Freya ChoiBR
ORD-2025-0015812025-12-28 06:12:382645.06Jonas LeeJP
ORD-2025-0009952025-12-28 05:47:532584.02Leo HaddadUS

ブラウザの別タブで 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;
Planche で実行

結果 — 25 行のうち先頭 5 行

order_numberproductquantityunit_price
ORD-2025-000505Zen Boots1700.65
ORD-2025-000505Echo Backpack 40L Plus11570.96
ORD-2025-001581Atlas Phone Pro Max11546.38
ORD-2025-001581Pulse Headphones2548.09
ORD-2025-000995Aria Chrono21289.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;
Planche で実行

結果 — 25 行のうち先頭 5 行

idfirst_namelast_nameorder_numbertotal
1LiamFischerORD-2022-0015961317.56
1LiamFischerORD-2025-0017232664.56
1LiamFischerORD-2025-0031424609.77
2MateoCostaORD-2025-0024733760.6
3YukiJangORD-2025-0035277569.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;
Planche で実行

結果 — 25 行のうち先頭 5 行

idfirst_namelast_namecountry
14MayaMoreauUS
17NoraMüllerIT
20SanaParkKR
30RaviParkKR
33OliverParkIN

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;
Planche で実行

結果 — 25 行のうち先頭 5 行

idfirst_nameorder_numberstatus
1LiamNULLNULL
2MateoNULLNULL
3YukiORD-2025-003530cancelled
4InesNULLNULL
5LeoNULLNULL

条件が 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;
Planche で実行

結果 — 25 行のうち先頭 5 行

idfirst_nameorder_numberstatus
3YukiORD-2025-003530cancelled
6NoraORD-2023-001551cancelled
6NoraORD-2022-002224cancelled
7PriyaORD-2022-003262cancelled
10ElenaORD-2025-000182cancelled

こちらの条件は 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;
Planche で実行

結果 — 25 行のうち先頭 5 行

employeepositionmanager
Ava MüllerDirectorNULL
Hana TanakaDirectorNULL
Hugo AhmedDirectorNULL
Lucas RomeroDirectorNULL
Maya MüllerDirectorNULL

e と m は同じテーブルを2回見ているだけで、成立させているのは別名です。LEFT JOIN にしてあるのは、上層の10人には上司がいないからです。内部結合ではその10人が黙って消えます。

ここまでで書けるもの

スキーマ全体をまたいだ質問ができるようになりました。まだできないのは、その答えを要約することです。注文を数える、売上を合計する、平均を出す。それが次のレッスンです。

自分で書いてみる

  1. 直近のレビュー15件を、書かれた日時、対象の商品、書いた人、評価とともに。テーブルは3つです。

    答えを見る

    答えを見る

    SELECT r.created_at,
           p.name AS product,
           c.first_name || ' ' || c.last_name AS reviewer,
           r.rating
    FROM reviews r
    JOIN products p ON p.id = r.product_id
    JOIN customers c ON c.id = r.customer_id
    ORDER BY r.created_at DESC
    LIMIT 15;
    Planche で実行

    結果 — 15 行のうち先頭 5 行

    created_atproductreviewerrating
    2025-12-28 21:12:12Sol ChronoHugo Park2
    2025-12-28 16:46:22Ember Catan 2Minjun Novak4
    2025-12-28 14:00:00Quill SpeakerOmar Smith3
    2025-12-28 03:41:42Learning React (Deluxe ed.)Ines Romero4
    2025-12-28 03:40:06Zen Sheet Set 2Kenji Ahmed4

    結合を2つつなげます。レビューと商品で商品名を、レビューと顧客で書いた人を取ります。レビューには必ず両方があるので、どちらも内部結合です。

  2. 出荷記録がまったくない注文はどれですか。

    答えを見る

    答えを見る

    SELECT o.order_number, o.status, o.ordered_at
    FROM orders o
    LEFT JOIN shipments s ON s.order_id = o.id
    WHERE s.id IS NULL
    ORDER BY o.ordered_at DESC
    LIMIT 20;
    Planche で実行

    結果 — 20 行のうち先頭 5 行

    order_numberstatusordered_at
    ORD-2025-000940cancelled2025-12-28 07:28:46
    ORD-2025-003297processing2025-12-28 01:40:38
    ORD-2025-002729cancelled2025-12-27 23:10:11
    ORD-2025-002019paid2025-12-27 09:26:48
    ORD-2025-000337pending2025-12-27 05:29:40

    LEFT JOIN で注文を全部残し、WHERE s.id IS NULL で一致しなかったものだけを残します。4,000件中1,681件が該当します。保留中やキャンセル済みの注文は発送されないので、妥当な数字です。