EP03. "JOIN 全解析与 UNION"
🔒 登录后可标记已读- 电商系统的数据天生分散在多张表——用户、订单、商品各自独立,JOIN 的方向选错一个字,报表数字就会离谱到自己都不敢信。
- EP01 已经详细讲过 INNER JOIN 和 LEFT JOIN,这篇只用一句话复习,重点补齐没讲的 RIGHT JOIN、FULL JOIN、SELF JOIN。
- UNION / UNION ALL 是全新主题,方向跟 JOIN 完全不同——JOIN 是"横向拼栏位",UNION 是"纵向叠结果"。
- 学完能分清 5 种 JOIN 分别保留哪一边的数据,也知道什么时候该用 UNION 去重、什么时候该用 UNION ALL 保留全部。
重点内容
复习:INNER JOIN 与 LEFT JOIN(EP01 已详细讲过)
- INNER JOIN:两边都要有匹配才留下,不匹配的行整行消失。
- LEFT JOIN:左表全部保留,右表配不到就补 NULL。
-- 复习:只要有下单记录的用户(INNER JOIN)
SELECT u.name, o.total_amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- 复习:所有用户都列出来,没下单的订单数算 0(LEFT JOIN)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;
RIGHT JOIN:保留右表全部
RIGHT JOIN 跟 LEFT JOIN 是镜像关系——保留右表(第二张表)的全部行,左表配不到就补 NULL。写法上只是把 LEFT JOIN 的两张表顺序倒过来,很多人干脆改写成 LEFT JOIN 让语句更好读,这也是 RIGHT JOIN 在实务上比较少见的原因。
典型场景:想知道哪些商品分类目前完全没有商品上架(分类表要全部保留,商品配不到也要看到)。
-- 保留 categories 全部分类,即使底下还没有任何商品
SELECT c.name AS category_name, p.name AS product_name
FROM products p
RIGHT JOIN categories c ON p.category_id = c.id;
-- 只挑出「一个商品都没有」的空分类
SELECT c.name AS category_name
FROM products p
RIGHT JOIN categories c ON p.category_id = c.id
WHERE p.id IS NULL;
FULL (OUTER) JOIN:两边都要保留
FULL JOIN 保留两张表的全部行,任一边配不到都补 NULL,等于 LEFT JOIN 和 RIGHT JOIN 的结果叠在一起。适合用来做数据完整性排查——一次看清「左表有、右表没有」和「右表有、左表没有」这两种异常。
-- 一次找出「从没下过单的用户」+「关联不到任何用户的孤儿订单」
SELECT u.id AS user_id, u.email, o.id AS order_id
FROM users u
FULL JOIN orders o ON u.id = o.user_id
WHERE u.id IS NULL OR o.user_id IS NULL;
📌 MySQL 不支持 FULL JOIN:写了会直接报语法错误。要在 MySQL 上做到同样效果,得自己用 LEFT JOIN 和 RIGHT JOIN 各查一次,再用 UNION 拼起来(UNION 的写法见下面):
-- MySQL 里模拟 FULL JOIN 的写法
SELECT u.id AS user_id, u.email, o.id AS order_id
FROM users u LEFT JOIN orders o ON u.id = o.user_id
UNION
SELECT u.id AS user_id, u.email, o.id AS order_id
FROM users u RIGHT JOIN orders o ON u.id = o.user_id;
SELF JOIN:跟自己接
SELF JOIN 不是新的关键字,而是把同一张表当成两张表来 JOIN,专门用来处理表内部的层级/关联关系。因为引用的是同一张表,一定要给两边取不同的别名,否则数据库分不清 SELECT/WHERE 里写的栏位是指哪一份。
典型场景:categories 表加一个 parent_id 栏位记录父分类(例如「电子产品」底下有「手机」「笔电」两个子分类)。
-- categories 表结构:id, name, parent_id(顶层分类的 parent_id 是 NULL)
SELECT
child.name AS 子分类,
parent.name AS 父分类
FROM categories child
JOIN categories parent ON child.parent_id = parent.id;
5 种 JOIN 一次看懂
flowchart TD
subgraph S1["🔗 INNER JOIN"]
direction TB
N1["只留两边都有的行<br/>任一边缺席就整行消失"]
end
subgraph S2["⬅️ LEFT JOIN"]
direction TB
N2["左表全留<br/>右表配不到补 NULL"]
end
subgraph S3["➡️ RIGHT JOIN"]
direction TB
N3["右表全留<br/>左表配不到补 NULL"]
end
flowchart TD
subgraph S4["↔️ FULL JOIN"]
direction TB
N4["两边全留<br/>任一边配不到都补 NULL"]
end
subgraph S5["🪞 SELF JOIN"]
direction TB
N5["同一张表当两张表用<br/>处理表内部的层级/关联"]
end
UNION:合并查询结果并去重
UNION 跟 JOIN 是完全不同方向的操作——JOIN 是把多张表的栏位横向拼在同一行,UNION 是把多个 SELECT 查询的结果纵向叠成一份结果集,还会自动去掉重复的整行数据。
用 UNION 有三个硬性条件:
- 每个 SELECT 的栏位数量要一样
- 对应栏位的数据类型要相容
- 栏位的顺序要一致(结果的栏位名称以第一个 SELECT 为准)
典型场景:把两份不同条件产生的名单合并成一份,还能用一个固定文字栏位标注「这行是哪个名单来的」。
-- 名单一:近 30 天消费超过 RM 500 的大客户
SELECT u.email, '大客户' AS 名单原因
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= NOW() - INTERVAL '30 days'
GROUP BY u.id, u.email
HAVING SUM(o.total_amount) > 500
UNION
-- 名单二:注册超过 90 天、从来没下过单的沉睡用户
SELECT u.email, '沉睡用户待唤醒' AS 名单原因
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL
AND u.created_at <= NOW() - INTERVAL '90 days';
💡 UNION 因为要去重,数据库要多做一次排序/比对的工作,行数一大会比 UNION ALL 慢——确定两份结果集本来就不会重复,或就是想保留重复,直接用 UNION ALL 更省效能。
UNION ALL:合并查询结果,保留全部(含重复)
跟 UNION 唯一的差别:UNION ALL 不去重,两个 SELECT 的结果原封不动叠在一起,就算整行数据完全相同也都保留。
沿用上面的名单例子——如果同一个用户刚好同时符合两份名单的条件(几率很低,但假设业务需求就是要各自发一封对应的通知邮件,而不是合并成一条),改用 UNION ALL:
SELECT u.email, '大客户' AS 名单原因
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= NOW() - INTERVAL '30 days'
GROUP BY u.id, u.email
HAVING SUM(o.total_amount) > 500
UNION ALL
SELECT u.email, '沉睡用户待唤醒' AS 名单原因
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL
AND u.created_at <= NOW() - INTERVAL '90 days';
语法速查
| 任务 | 语法 |
|---|---|
| 保留右表全部 | FROM a RIGHT JOIN b ON a.id = b.a_id |
| 两表都保留 | FROM a FULL JOIN b ON a.id = b.a_id |
| 表跟自己接 | FROM t AS t1 JOIN t AS t2 ON t1.parent_id = t2.id |
| 合并结果并去重 | SELECT ... UNION SELECT ... |
| 合并结果保留全部 | SELECT ... UNION ALL SELECT ... |
适用版本
内容以 PostgreSQL 语法为主,PostgreSQL 完整支持这 5 种 JOIN。跟其他数据库的差异整理如下:
| 数据库 | RIGHT JOIN | FULL JOIN |
|---|---|---|
| PostgreSQL / SQL Server | 支持 | 支持 |
| MySQL | 支持 | 不支持,需要用 LEFT JOIN UNION RIGHT JOIN 模拟 |
常见错误
- ❌ 遇到需求就下意识用 RIGHT JOIN——大多数情况下把两张表顺序对调、改回 LEFT JOIN 会更好读,团队协作时优先统一用 LEFT JOIN
- ❌ 在 MySQL 上直接照抄 PostgreSQL 的 FULL JOIN 语法——MySQL 没有这个关键字,要改用
LEFT JOIN+RIGHT JOIN搭配 UNION 模拟 - ❌ SELF JOIN 忘记给两边取别名,或者 WHERE 条件没排除自己跟自己配对(比如漏了
id1 <> id2),导致每一行都多配出一笔自己对自己的重复数据 - ❌ UNION 上下两个 SELECT 栏位数量或顺序对不上——数据库会直接报错,或者更麻烦的是栏位类型刚好都兼容、不报错但数据对错位置
- ❌ 明知道两份结果集不会重复,还是习惯性用 UNION——多余的去重排序会拖慢查询,该用 UNION ALL
Sources
Blog / Website: