在实际业务中,经常需要将两条或更多 SELECT 语句的结果拼接起来,作为一个整体返回。比如“查询所有已支付的订单和所有已取消的订单”、“将历史表与当前表的数据合并展示”等。SQL 提供了集合查询来处理这类需求,其中最常用的两个操作符是 UNION 和 UNION ALL。
9.5.1 基本概念与语法
集合查询的核心思想是:纵向拼接,而不是横向关联(那是 JOIN 的事)。
UNION 和 UNION ALL 都是将多条 SELECT 语句的行合并到一起,但处理重复行的方式不同:
- UNION:合并结果后去重,相当于数学上的并集。
- UNION ALL:合并结果后不去重,直接按顺序堆叠,性能更好。
基本语法如下:
SELECT 列1, 列2, ...
FROM 表A
WHERE 条件
UNION [ALL]
SELECT 列1, 列2, ...
FROM 表B
WHERE 条件;
关键要求:每条 SELECT 语句的列数量必须相同,并且对应位置的数据类型最好兼容(MySQL 会尝试隐式转换,但显式保持一致才是良好习惯)。最终列名默认取自第一条 SELECT 语句的列别名。
9.5.2 UNION 与 UNION ALL 的核心区别
用一个直观的例子来说明。
假设有两张表:
vip_users包含('Alice', 'VIP')、('Bob', 'VIP')regular_users包含('Bob', 'Regular')、('Charlie', 'Regular')
使用 UNION 查询所有用户:
SELECT name FROM vip_users
UNION
SELECT name FROM regular_users;
-- 结果: 'Alice', 'Bob', 'Charlie' (Bob 只出现一次)
而使用 UNION ALL:
SELECT name FROM vip_users
UNION ALL
SELECT name FROM regular_users;
-- 结果: 'Alice', 'Bob', 'Bob', 'Charlie' (Bob 出现两次)
背后的原理很简单:UNION 在合并结果后隐含地做了一次 DISTINCT 去重,这会引入额外的排序或临时表操作,数据量大时性能开销不低。UNION ALL 则直接拼接,无需去重,所以在不需要去重或明确知道不会产生重复的场景下,务必使用 UNION ALL,效率更高。
9.5.3 常见使用场景
- 多表结构相同的数据合并
比如按月分表存储订单:orders_202501、orders_202502,想要查询最近两个月的订单列表,就可以用 UNION ALL 把两个月的查询纵向拼起来(月表之间用户ID不会重复,但这里只是举例,如果订单号唯一,并不需要去重,此时 UNION ALL 正合适)。
SELECT order_id, amount FROM orders_202501
UNION ALL
SELECT order_id, amount FROM orders_202502;
- 不同状态的分类汇总
统计“已完成且金额>1000的订单”和“已退款且金额>1000的订单”的汇总,直接 UNION ALL 合并即可,因为两类不可能有重叠。
- 替代复杂的 OR 条件
某些场景下,OR 会导致全表扫描且结果可能会重复,拆成两个条件明确的查询然后用 UNION 合并,既可以利用不同的索引,又能天然去重,有时反而比一个复杂的 OR 查询快。
9.5.4 使用 UNION 时的注意事项
- 排序与 LIMIT 的位置
集合操作整体排序要用外层包裹的 ORDER BY,写在最后一条 SELECT 之后会被误解为只对最后一条生效。正确做法:
SELECT name, age FROM table1
UNION ALL
SELECT name, age FROM table2
ORDER BY age;
如果要给每条 SELECT 内部加排序或限制,记得用括号括起来:
(SELECT name, age FROM table1 ORDER BY age LIMIT 5)
UNION ALL
(SELECT name, age FROM table2 ORDER BY age LIMIT 5);
- 性能影响
UNION 的去重步骤可能需要创建临时表、进行全表扫描,如果数据量很大且无需去重,一定用 UNION ALL。即使需要去重,也可以考虑是否能在单条 SELECT 内通过 GROUP BY 等其他方式完成,或者是否真的需要数据库层面去重。
- 列数据类型要匹配
列数必须相同,列类型若不一致会触发隐式转换。例如一个查询返回 INT,另一个返回 VARCHAR,MySQL 会尝试转为统一类型,但可能出现意外错误或性能损失。最好通过 CAST 或 CONVERT 保证一致。
9.5.5 MySQL 8.0 新增的集合操作:INTERSECT 和 EXCEPT
MySQL 8.0 除了传统的 UNION、UNION ALL,还引入了 INTERSECT(交集)和 EXCEPT(差集,MySQL 中写法为 EXCEPT 但文档中也支持 MINUS 语法,不过实际 8.0 只有 EXCEPT,8.0.31 后还增加了 INTERSECT ALL 和 EXCEPT ALL,但一般用得不多)。
INTERSECT:返回两个查询结果中共同存在的行,且默认去重。EXCEPT:返回在第一个查询中有,第二个查询中没有的行(差集),也默认去重。
用法与 UNION 完全一致,只需替换关键字。例如找出既在 VIP 用户表又在本月活跃列表中的用户:
SELECT user_id FROM vip_users
INTERSECT
SELECT user_id FROM monthly_active_users;
如果需要保留重复行,可以使用 INTERSECT ALL 或 EXCEPT ALL(需要 MySQL 8.0.31+)。但绝大多数业务场景下,默认去重版已足够。
9.5.6 实际优化建议
- 在能确定结果无重复时,用
UNION ALL替代UNION,这是集合查询中性价比最高的优化。 - 每条子查询尽量独立优化,确保它们各自的索引使用良好。合并前的开销不能忽视。
- 不滥用集合查询:如果本质上是多个条件的组合,试试用
CASE WHEN或IN子句改写,可能更简洁高效。集合查询最适合结构不同或需要跨多个表/分区纯净拼接的情况。 - 注意列数量和对齐,避免因为疏忽导致列错位,返回错误数据。
集合查询是 SQL 基础中看似简单却极易被用错的地方。掌握 UNION 和 UNION ALL 的正确区别,并养成优先用 UNION ALL 的习惯,能让你的查询既安全又高效。