把表拼起来:JOIN

客户、订单和商品通常保存在不同的表中。JOIN 通过键把相关的行放到一次查询里读取。

最基本的连接

订单存的是 customer_id,客户表里才有名字。想一起看到,就要告诉数据库这两张表怎么对上。

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 之前,什么都不会执行。

这里有三样东西值得点名。单写 JOIN 就是 INNER JOIN。ON c.id = o.customer_id 是连接条件——左边的行按什么和右边的行配对。而 orders o 给表起了个短别名,正因为有它,你才能写 o.total 和 c.country,而不用每次重复整个表名。

一旦两张表有同名的列,别名就不再是可选项了。这两张表都有 id;不加限定的 id 是有歧义的,数据库会直接拒绝这条查询。

不止两张表

连接是可以串的。订单里的商品隔着两步: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 都是在已有结果上再挂一张表,所以四表连接不过是再加一行。从上往下读就是"订单,连上它的明细,再连上每条明细对应的商品"。

保留没有匹配的行

内连接会丢掉配不上对的一切。从没下过单的客户干脆就从结果里消失了。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 是主键,在表里绝不可能为空。但它在这个结果里可以为空,因为 LEFT JOIN 为那些配不上的行凭空造出了这些 NULL。这些行就是那 95 位从没下过单的客户。

ON 还是 WHERE:不是同一个问题

坑在这里。下面两条查询都写了 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,所以这张表可以用两个不同的别名和自己连接。

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 是同一张表被看了两次,让它成立的正是别名。这里用 LEFT JOIN,是因为最上面那十个人没有上级,用内连接会把他们悄悄漏掉。

现在你能做什么

你可以跨整套结构提问了。还做不到的是把答案汇总起来:数订单、算营收、求平均,都在下一课。

自己动手写

  1. 最新的 15 条评价,显示写于何时、评的哪个商品、谁写的、以及评分。这需要三张表。

    查看答案

    查看答案

    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. 哪些订单完全没有发货记录?

    查看答案

    查看答案

    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 张符合——这说得通,因为待处理和已取消的订单本来就不会发货。