集合操作 UNION 与 INTERSECT
本教程共 46 篇 · 第 26 篇 · 更新于 2026-07-30 · 约 10 分钟阅读
26. 集合操作 UNION 与 INTERSECT
本节目标:掌握 MySQL 的四种集合操作:UNION(合并去重)、UNION ALL(合并不去重)、INTERSECT(交集)、EXCEPT(差集),理解列匹配规则、排序规则和版本要求。
26.1 什么是集合操作
集合操作就是把多个 SELECT 查询的结果”拼”在一起。就像数学里的集合运算:并集、交集、差集。
MySQL 支持四种集合操作:
| 操作 | 含义 | 版本要求 |
|---|---|---|
UNION | 合并两个结果集,自动去重 | 所有版本 |
UNION ALL | 合并两个结果集,不去重 | 所有版本 |
INTERSECT | 取两个结果集的交集 | 8.0+ |
EXCEPT | 取第一个结果集减去第二个的差集 | 8.0+ |
Warning
INTERSECT和EXCEPT是 MySQL 8.0 才引入的。5.7 及更早版本不支持,会报语法错误。26.7 当然支持。如果需要兼容 5.7,用 JOIN 或 IN 子查询替代。
26.2 UNION:合并去重
UNION 把两个查询的结果上下拼接,重复的行只保留一条:
-- 合并用户名和订单状态,看看有哪些不同的"标签"
SELECT username AS name FROM users
UNION
SELECT status AS name FROM orders;
第一个查询返回所有用户名,第二个返回所有订单状态。UNION 把它们合并成一张列表,重复的去掉。
更实际的例子:
-- 查消费超过 500 的用户 和 年龄小于 20 的用户(去重合并)
SELECT * FROM users WHERE id IN (
SELECT user_id FROM orders WHERE amount > 500
)
UNION
SELECT * FROM users WHERE age < 20;
两个查询的结果合并,如果某个用户同时满足两个条件(消费超 500 且年龄小于 20),只出现一次。
26.3 UNION ALL:合并不去重
UNION ALL 跟 UNION 一样合并结果,但不去重,重复的行全部保留:
SELECT username AS name FROM users
UNION ALL
SELECT status AS name FROM orders;
如果两个查询有相同的行,UNION ALL 会全部保留。
UNION vs UNION ALL 性能对比
| 对比项 | UNION | UNION ALL |
|---|---|---|
| 去重 | 是 | 否 |
| 性能 | 慢(需要去重排序) | 快 |
| 结果可能有重复 | 不会 | 会 |
Tip如果确定两个查询结果不会有重复行,或者你不在乎重复,用 UNION ALL。UNION 需要额外的去重操作(通常用临时表),性能比 UNION ALL 差。我之前优化过一个查询,把 UNION 改成 UNION ALL,从 800ms 降到 50ms,就因为省去了去重。
26.4 集合操作的列匹配规则
参与集合操作的多个查询,必须满足以下条件:
- 列数相同:每个 SELECT 的列数必须一样。
- 列类型兼容:对应位置的列类型要能隐式转换。
- 列名取第一个:结果集的列名用第一个 SELECT 的列名。
-- 正确:两个查询都是 2 列,类型兼容
SELECT username, age FROM users
UNION
SELECT status, amount FROM orders;
-- 结果列名是 username 和 age(取第一个查询的)
-- 错误:列数不同
SELECT username, age, email FROM users
UNION
SELECT status, amount FROM orders;
-- ERROR: The used SELECT statements have a different number of columns
Note列名取第一个 SELECT 的。如果你想给结果起有意义的列名,在第一个 SELECT 里用 AS 起别名就行:
SELECT username AS 名称, age AS 数值 FROM users UNION SELECT status, amount FROM orders; -- 结果列名是"名称"和"数值"
26.5 INTERSECT:交集
INTERSECT 返回两个结果集都有的行(交集),自动去重:
-- 查既在 users 表又在 vip_users 表中的用户
SELECT id, username FROM users
INTERSECT
SELECT id, username FROM vip_users;
只在一张表里有的行不会出现。
-- 查有 paid 订单且有 shipped 订单的用户
SELECT user_id FROM orders WHERE status = 'paid'
INTERSECT
SELECT user_id FROM orders WHERE status = 'shipped';
用 IN 子查询替代 INTERSECT(5.7 兼容)
5.7 不支持 INTERSECT,可以用 IN 或 EXISTS 替代:
-- INTERSECT 等价写法
SELECT DISTINCT user_id FROM orders
WHERE status = 'paid'
AND user_id IN (
SELECT user_id FROM orders WHERE status = 'shipped'
);
26.6 EXCEPT:差集
EXCEPT 返回第一个结果集有但第二个没有的行,自动去重:
-- 查在 users 表但不在 vip_users 表中的用户
SELECT id, username FROM users
EXCEPT
SELECT id, username FROM vip_users;
-- 查有 pending 订单但没有 paid 订单的用户
SELECT user_id FROM orders WHERE status = 'pending'
EXCEPT
SELECT user_id FROM orders WHERE status = 'paid';
用 NOT IN 替代 EXCEPT(5.7 兼容)
-- EXCEPT 等价写法
SELECT DISTINCT user_id FROM orders
WHERE status = 'pending'
AND user_id NOT IN (
SELECT user_id FROM orders WHERE status = 'paid' AND user_id IS NOT NULL
);
Warning用 NOT IN 替代 EXCEPT 时别忘了排除 NULL(加
IS NOT NULL),否则可能返回空结果。这是第 20 章讲过的 NULL 陷阱。
26.7 四种操作对比
| 操作 | 结果 | 去重 | 版本 |
|---|---|---|---|
| UNION | A + B | 是 | 所有版本 |
| UNION ALL | A + B | 否 | 所有版本 |
| INTERSECT | A ∩ B | 是 | 8.0+ |
| EXCEPT | A - B | 是 | 8.0+ |
用维恩图来理解:
UNION: A 和 B 的所有元素
UNION ALL: A 和 B 的所有元素(含重复)
INTERSECT: A 和 B 都有的元素
EXCEPT: A 有但 B 没有的元素
26.8 集合操作的排序
集合操作的结果默认无序。如果需要排序,ORDER BY 只能写在最后面,作用于整个合并后的结果:
-- 正确:ORDER BY 在最后
SELECT username, age FROM users WHERE age > 25
UNION
SELECT username, age FROM users WHERE age < 20
ORDER BY age DESC;
-- 错误:不能在中间的 SELECT 加 ORDER BY
SELECT username, age FROM users WHERE age > 25 ORDER BY age
UNION
SELECT username, age FROM users WHERE age < 20;
-- 语法错误
Note如果想在合并前分别排序,可以把每个查询包成派生表,在派生表内部排序,再 UNION。但这通常没有意义,因为合并后的顺序不保证。排序应该在最后统一做。
26.9 集合操作 vs JOIN
集合操作和 JOIN 都能组合多个查询的数据,但原理完全不同:
| 对比项 | 集合操作(UNION 等) | JOIN |
|---|---|---|
| 组合方向 | 上下拼接(增加行) | 左右拼接(增加列) |
| 列数要求 | 两个查询列数相同 | 无限制 |
| 数据来源 | 通常来自不同表或不同条件 | 来自关联的多张表 |
| 重复处理 | UNION 去重,ALL 不去 | JOIN 可能产生重复行 |
UNION 是"竖着拼"(行变多,列不变)
JOIN 是"横着拼"(列变多,行取决于匹配)
Tip简单记忆:UNION 是把两张表”摞起来”,JOIN 是把两张表”拼起来”。需要更多行用 UNION,需要更多列用 JOIN。
26.10 实用示例
示例一:合并多张结构相同的表
-- 假设有 2025 年和 2026 年的订单分别存在两张表
SELECT * FROM orders_2025
UNION ALL
SELECT * FROM orders_2026
ORDER BY created_at DESC;
用 UNION ALL 因为两年不会有重复数据,省去去重开销。
示例二:同时查活跃用户和 VIP 用户
SELECT id, username, '活跃' AS 类型 FROM users
WHERE id IN (SELECT user_id FROM orders WHERE created_at > '2026-07-01')
UNION
SELECT id, username, 'VIP' AS 类型 FROM users
WHERE age > 30;
加了一个常量列 类型 来标识每行来自哪个查询,方便后续区分。
示例三:找只出现在一张表中的数据
-- 找没有订单的用户
SELECT id, username FROM users
EXCEPT
SELECT DISTINCT user_id, NULL FROM orders;
-- 注意:这里列数和类型要匹配,可能需要调整
Note上面的 EXCEPT 写法不够直观(需要处理列匹配)。实际上”找没有订单的用户”用 NOT EXISTS 更自然:
SELECT id, username FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);集合操作不是万能的,选择最合适的工具。
26.11 小结
UNION 合并去重,UNION ALL 合并不去重(更快),INTERSECT 取交集,EXCEPT 取差集。后两者是 8.0+ 特性。核心记住:参与集合操作的查询列数必须相同、列名取第一个查询的、ORDER BY 只能写在最后、确定无重复就用 UNION ALL。集合操作是”竖着拼”,JOIN 是”横着拼”,别搞混。下一章学多表连接 JOIN。