多表 JOIN
一个订单买了什么,要看三张表:orders 记着谁哪天下的单,order_items 记着每单买了哪件商品、几件、单价,products 才有商品的名字。这一课把好几张表接在一起,再在接好的结果上分组。
三张表
JOIN 可以一个接一个地写。下面这条查出 1 号客户每个订单里的商品:
1SELECT o.id AS order_id, o.order_date, p.name, oi.quantity2FROM orders AS o3JOIN order_items AS oi ON oi.order_id = o.id4JOIN products AS p ON p.id = oi.product_id5WHERE o.customer_id = 16ORDER BY o.id, p.id;
| order_id | order_date | name | quantity |
|---|---|---|---|
| 1 | 2025-09-02 | 笔记本 | 3 |
| 1 | 2025-09-02 | 中性笔(10 支) | 1 |
| 3 | 2025-09-18 | 耳机 | 1 |
| 3 | 2025-09-18 | 数据线 | 2 |
| 9 | 2025-10-15 | 笔记本 | 2 |
| 9 | 2025-10-15 | 中性笔(10 支) | 2 |
读的时候从上往下:先是 orders;第一个 JOIN 给每个订单配上它的明细,一行明细一行;第二个 JOIN 再给每行明细配上它的商品。每个 JOIN 都是拿「到目前为止的结果」去和下一张表配对,ON 写的是这一步怎么配。
点单步,看两个 JOIN 一步一步地把列加上去。
四张表
再接上 customers,就能看到客户的名字。下面是上海客户买过的东西,amount 是这一行花的钱:
1SELECT c.name, o.id AS order_id, p.name AS product,2 oi.quantity * oi.unit_price AS amount3FROM customers AS c4JOIN orders AS o ON o.customer_id = c.id5JOIN order_items AS oi ON oi.order_id = o.id6JOIN products AS p ON p.id = oi.product_id7WHERE c.city = '上海'8ORDER BY c.id, o.id, p.id;
| name | order_id | product | amount |
|---|---|---|---|
| 林晓 | 1 | 笔记本 | 13.50 |
| 林晓 | 1 | 中性笔(10 支) | 6.00 |
| 林晓 | 3 | 耳机 | 59.00 |
| 林晓 | 3 | 数据线 | 17.00 |
| 林晓 | 9 | 笔记本 | 9.00 |
| 林晓 | 9 | 中性笔(10 支) | 12.00 |
| 陈静 | 6 | 笔记本 | 22.50 |
| 周婷 | 10 | 数据线 | 8.50 |
| 周婷 | 10 | 手机壳 | 15.00 |
c.name 和 p.name 都叫 name,所以给商品名起了别名 product。表多了,同名的列就多,列名前面都写上表的别名,不容易乱。
先 JOIN,再分组
现在算每个客户一共花了多少钱。这是最容易出错的地方,慢慢来。
先不分组,只看 JOIN 之后的行。下面只留 1 号林晓:
1SELECT c.name, o.id AS order_id, p.name AS product,2 oi.quantity * oi.unit_price AS amount3FROM customers AS c4JOIN orders AS o ON o.customer_id = c.id5JOIN order_items AS oi ON oi.order_id = o.id6JOIN products AS p ON p.id = oi.product_id7WHERE c.id = 18ORDER BY o.id, p.id;
| name | order_id | product | amount |
|---|---|---|---|
| 林晓 | 1 | 笔记本 | 13.50 |
| 林晓 | 1 | 中性笔(10 支) | 6.00 |
| 林晓 | 3 | 耳机 | 59.00 |
| 林晓 | 3 | 数据线 | 17.00 |
| 林晓 | 9 | 笔记本 | 9.00 |
| 林晓 | 9 | 中性笔(10 支) | 12.00 |
林晓有 3 个订单,可这里有 6 行。JOIN 之后,一行是一件商品的明细,不是一个订单,也不是一个客户。她花的钱就是这 6 行 amount 加起来。
再去掉 WHERE,按客户分组:
1SELECT c.name, SUM(oi.quantity * oi.unit_price) AS spent2FROM customers AS c3JOIN orders AS o ON o.customer_id = c.id4JOIN order_items AS oi ON oi.order_id = o.id5GROUP BY c.id, c.name6ORDER BY spent DESC;
| name | spent |
|---|---|
| 林晓 | 116.50 |
| 王磊 | 101.80 |
| 孙强 | 59.00 |
| 刘洋 | 55.80 |
| 张伟 | 54.60 |
| 周婷 | 23.50 |
| 陈静 | 22.50 |
点单步,一步一步看:
- FROM 和两个 JOIN:得到 18 行,每行一件商品的明细,前面带着客户和订单。
- GROUP BY:这 18 行按客户分组。林晓那一组是上面那 6 行。
- SELECT:每组算一个
SUM,变成一行。
GROUP BY 分的是 JOIN 之后的行,不是 customers 表本来的行。这条查询不需要商品名,所以没有接 products。GROUP BY 里写了 c.id,是因为可能有两个客户同名。没下过单的客户不在结果里;要他们也出现,用上一课的 LEFT JOIN。
数订单数的坑
在同一条查询里再数一下每人几单,大多数人会这样写:
林晓下过 3 单。这条查询给她数出几?
1SELECT c.name, COUNT(*) AS orders2FROM customers AS c3JOIN orders AS o ON o.customer_id = c.id4JOIN order_items AS oi ON oi.order_id = o.id5WHERE c.id = 16GROUP BY c.id, c.name;
COUNT(*) 数的是组里有几行。JOIN 了 order_items 以后一行是一件商品的明细,林晓那一组有 6 行,就数出 6。11 是她买的件数,要 SUM(oi.quantity)。
要数订单,就数「不同的订单编号有几个」:第 7 课的 COUNT(DISTINCT o.id)。DISTINCT 放在括号里,先去掉重复的值再数。把两种写法放在一起比:
1SELECT c.name, COUNT(*) AS item_rows, COUNT(DISTINCT o.id) AS orders2FROM customers AS c3JOIN orders AS o ON o.customer_id = c.id4JOIN order_items AS oi ON oi.order_id = o.id5GROUP BY c.id, c.name6ORDER BY c.id;
| name | item_rows | orders |
|---|---|---|
| 林晓 | 6 | 3 |
| 王磊 | 3 | 2 |
| 陈静 | 1 | 1 |
| 张伟 | 2 | 2 |
| 刘洋 | 3 | 2 |
| 孙强 | 1 | 1 |
| 周婷 | 2 | 1 |
小技巧: 写 JOIN 加 GROUP BY 之前,先不分组跑一次,问自己「现在一行是什么」。一行是订单明细,COUNT(*) 数的就是明细,不是订单。
自己 JOIN 自己
员工表里的 manager_id 是上级的员工编号,上级也在这张表里。要把每个人和上级的名字放在一行,就让 employees 和自己 JOIN。同一张表用两次,起两个不同的别名:e 是员工,m 是上级:
1SELECT e.name, e.title, m.name AS manager2FROM employees AS e3JOIN employees AS m ON e.manager_id = m.id4ORDER BY e.id;
| name | title | manager |
|---|---|---|
| 马超 | 销售主管 | 李华 |
| 杨帆 | 仓库主管 | 李华 |
| 黄磊 | 销售 | 马超 |
| 何琳 | 销售 | 马超 |
| 高远 | 打包员 | 杨帆 |
只有 5 行:1 号经理李华没有上级,manager_id 是 NULL,配不上任何人。想让她也出现,还是用 LEFT JOIN:
1SELECT e.name, e.title, m.name AS manager2FROM employees AS e3LEFT JOIN employees AS m ON e.manager_id = m.id4ORDER BY e.id;
| name | title | manager |
|---|---|---|
| 李华 | 经理 | NULL |
| 马超 | 销售主管 | 李华 |
| 杨帆 | 仓库主管 | 李华 |
| 黄磊 | 销售 | 马超 |
| 何琳 | 销售 | 马超 |
| 高远 | 打包员 | 杨帆 |
练一练
写好查询,点检查。提示随便看,实在想不出来再看答案。
每个分类卖了多少
✓算出每个商品分类的销售额(数量 × 单价 加起来,取消的订单也算):category 和 revenue 两列,按 revenue 从高到低排。
| category | revenue |
|---|---|
| 数码 | 173.50 |
| 箱包 | 101.80 |
| 厨房 | 95.40 |
| 文具 | 63.00 |
1SELECT oi.order_id, oi.quantity * oi.unit_price AS amount2FROM order_items AS oi;
几单、几件
✓对每个下过单的客户,查出 name、下了几单 orders、一共买了几件 items,按客户 id 排序。起始代码的 orders 数错了,改好它。
| name | orders | items |
|---|---|---|
| 林晓 | 3 | 11 |
| 王磊 | 2 | 4 |
| 陈静 | 1 | 5 |
| 张伟 | 2 | 5 |
| 刘洋 | 2 | 5 |
| 孙强 | 1 | 1 |
| 周婷 | 1 | 2 |
1SELECT c.name, COUNT(*) AS orders, SUM(oi.quantity) AS items2FROM customers AS c3JOIN orders AS o ON o.customer_id = c.id4JOIN order_items AS oi ON oi.order_id = o.id5GROUP BY c.id, c.name6ORDER BY c.id;
每个上级管几个人
✓查出每个有下属的人,和他直接管几个人:manager 和 reports 两列,按上级的 id 排序。
| manager | reports |
|---|---|
| 李华 | 2 |
| 马超 | 2 |
| 杨帆 | 1 |
1SELECT e.name, m.name AS manager2FROM employees AS e3JOIN employees AS m ON e.manager_id = m.id4ORDER BY e.id;
这一课学会了
JOIN可以一个接一个地写,每个都拿到目前为止的结果去配下一张表。- 表多了,列名前写上表的别名,同名的列起别名。
- JOIN 加 GROUP BY:先 JOIN,再把 JOIN 之后的行分组。先不分组跑一次,看清「一行是什么」。
- 一行是明细时,
COUNT(*)数的是明细;数订单用COUNT(DISTINCT o.id)。 - 一张表可以和自己 JOIN,起两个不同的别名;想留下配不上的行,用
LEFT JOIN。