首页 / MySQL 入门教程 / 集合操作 UNION 与 INTERSECT

MySQL 入门教程

集合操作 UNION 与 INTERSECT

本教程共 46 篇 · 第 26 篇 · 更新于 2026-07-30 · 约 10 分钟阅读

MySQLMySQL 入门教程UNIONUNION ALLINTERSECTEXCEPT集合操作

26. 集合操作 UNION 与 INTERSECT

本节目标:掌握 MySQL 的四种集合操作:UNION(合并去重)、UNION ALL(合并不去重)、INTERSECT(交集)、EXCEPT(差集),理解列匹配规则、排序规则和版本要求。

26.1 什么是集合操作

集合操作就是把多个 SELECT 查询的结果”拼”在一起。就像数学里的集合运算:并集、交集、差集。

MySQL 支持四种集合操作:

操作含义版本要求
UNION合并两个结果集,自动去重所有版本
UNION ALL合并两个结果集,不去重所有版本
INTERSECT取两个结果集的交集8.0+
EXCEPT取第一个结果集减去第二个的差集8.0+
Warning

INTERSECTEXCEPT 是 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 性能对比

对比项UNIONUNION ALL
去重
性能慢(需要去重排序)
结果可能有重复不会
Tip

如果确定两个查询结果不会有重复行,或者你不在乎重复,用 UNION ALL。UNION 需要额外的去重操作(通常用临时表),性能比 UNION ALL 差。我之前优化过一个查询,把 UNION 改成 UNION ALL,从 800ms 降到 50ms,就因为省去了去重。

26.4 集合操作的列匹配规则

参与集合操作的多个查询,必须满足以下条件:

  1. 列数相同:每个 SELECT 的列数必须一样。
  2. 列类型兼容:对应位置的列类型要能隐式转换。
  3. 列名取第一个:结果集的列名用第一个 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 四种操作对比

操作结果去重版本
UNIONA + B所有版本
UNION ALLA + B所有版本
INTERSECTA ∩ B8.0+
EXCEPTA - B8.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。