UNION 与 UNION ALL 的区别?为什么 UNION ALL 更快?
What is the difference between UNION and UNION ALL? Why is UNION ALL faster?
考察要点
这道题面试官想看你是否懂 SQL 集合操作的精确语义、其底层实现(尤其是去重操作)的性能开销,以及在业务场景中进行性能与功能权衡的能力。回答此题能体现你是否只停留在“知其然”(知道会去重),还是达到了“知其所以然”(理解为什么慢,以及慢多少)的深度。
核心概念
UNION 和 UNION ALL 都是 SQL 中用于合并两个或多个 SELECT 语句结果集的集合运算符。它们要求所有 SELECT 语句的列数相同,且对应列的数据类型兼容。
1. UNION
UNION 操作符合并结果集,并自动去除结果集中的重复行。它等价于将所有结果集合并后,再进行一次 DISTINCT 操作。
内部执行过程: 为了找出并删除重复行,数据库必须对合并后的整个结果集进行一次去重处理。这通常通过以下两种方式之一实现:
- 排序(Sort):将所有结果行放入一个临时空间,然后对所有行进行排序,最后遍历排序后的结果,只保留唯一的行。
- 哈希(Hash):将结果行逐一放入一个哈希表。在放入前,检查该行是否已存在于哈希表中。如果不存在,则放入哈希表并加入最终结果集;如果已存在,则跳过。
这两种方式都需要额外的 CPU 和内存资源,当数据量巨大时,临时空间可能不足,导致使用磁盘(filesort),性能会急剧下降。
2. UNION ALL
UNION ALL 操作符合并结果集,但它不进行任何去重操作,直接将第二个查询的结果追加到第一个查询结果的末尾。
内部执行过程:
这是一个非常直接的操作。数据库依次执行每个 SELECT 语句,并将结果流式地、不加处理地追加到最终结果集中。它不涉及行与行之间的比较,因此没有排序或哈希的开销。
示例与对比
假设我们有两张表 employees_cn(中国区员工)和 employees_us(美国区员工)。
employees_cn
| id | name |
|---|---|
| 1 | Alice |
| 2 | Bob |
employees_us
| id | name |
|---|---|
| 2 | Bob |
| 3 | Charlie |
UNION 查询:
1SELECT id, name FROM employees_cn2UNION3SELECT id, name FROM employees_us;
结果(去重后):
| id | name |
|---|---|
| 1 | Alice |
| 2 | Bob |
| 3 | Charlie |
UNION ALL 查询:
1SELECT id, name FROM employees_cn2UNION ALL3SELECT id, name FROM employees_us;
结果(直接合并):
| id | name |
|---|---|
| 1 | Alice |
| 2 | Bob |
| 2 | Bob |
| 3 | Charlie |
性能差异根源:为什么 UNION ALL 更快?
UNION 的执行计划 ≈ UNION ALL 的执行计划 + 去重操作(排序或哈希)
- CPU 开销:
UNION需要比较行数据来判断是否重复,消耗 CPU。UNION ALL不需要。 - 内存/磁盘 I/O 开销:
UNION需要一个临时工作区(内存或磁盘)来执行排序或建立哈希表。UNION ALL直接输出结果,几乎没有额外内存开销。
因此,UNION ALL 的性能总是优于 UNION,除非结果集本身非常小,差异可以忽略不计。
常见低分回答
- 回答过于肤浅:“
UNION去重,UNION ALL不去重”。这只说出了表面现象,没有解释背后的性能原理,无法体现深度。 - 断言
UNION一定会排序:“UNION会对结果进行排序,所以慢”。虽然排序是去重的常用方法,但数据库也可能使用哈希算法。这种不严谨的表述会显得知识面不够广。 - 滥用
UNION:在业务逻辑上可以保证结果集不会有重复的情况下,仍然使用UNION。例如:SELECT id FROM users WHERE status = 'active' UNION SELECT id FROM users WHERE status = 'inactive'。因为一个用户的status不可能同时是active和inactive,所以结果天然无重复,此处使用UNION是不必要的性能浪费,应使用UNION ALL。 - 混淆
UNION和JOIN:说出“UNION是把两个表横向拼起来”之类的错误描述。UNION是纵向合并行,JOIN是横向合并列。 - 忽略
UNION对ORDER BY的影响:认为可以在UNION的每个子查询中单独使用ORDER BY。在大多数数据库中,ORDER BY只能用于整个UNION结果集的末尾,用于对最终结果排序。子查询中的ORDER BY通常会被忽略或报错(除非配合LIMIT)。
工程实践与优化
- 黄金法则:能用
UNION ALL就绝不用UNION。在编写 SQL 时,应首先思考业务逻辑是否能保证结果的唯一性。如果能,就必须使用UNION ALL,这是基本的 SQL素养。 - 执行计划分析:通过
EXPLAIN命令可以清晰地看到二者的区别。- 对于
UNION查询,EXPLAIN的Extra列通常会显示Using temporary,表示为了去重创建了临时表。 - 对于
UNION ALL查询,则没有这个步骤。
sql1-- MySQL 8.0+2EXPLAIN FORMAT=TREE3SELECT id FROM employees_cn4UNION5SELECT id FROM employees_us;67-- 输出可能包含 -> Table scan on <temporary> (cost=... rows=...)8-- -> Deduplicate rows - 对于
- 大数据量场景:当合并上亿级别的数据行时,
UNION带来的Using temporary和filesort可能是灾难性的,它会消耗大量内存并可能将临时数据写入磁盘,导致查询时间从秒级恶化到分钟级甚至小时级。UNION ALL则能保持流式处理,性能稳定。 - 索引优化:
UNION或UNION ALL本身无法直接利用索引。但其包含的每个SELECT子查询都应该被独立优化。确保每个子查询的WHERE条件、JOIN条件都能有效利用索引,是整个查询高性能的基础。
常见追问与延伸题目
-
追问:如果业务上必须去重,但
UNION性能太差,有什么优化方案?- 回答要点 1 (应用层去重):如果返回给应用层的数据量不大(例如几千到几万行),可以考虑使用
UNION ALL将所有数据(包括重复的)快速返回给应用,由应用层代码(如 Java 的HashSet或 Python 的set)进行去重。这是一种将数据库的计算压力转移到应用服务器的策略。 - 回答要点 2 (
GROUP BY替代):可以使用UNION ALL将结果集合并,然后在外部查询中使用GROUP BY来去重。SELECT col1, col2, ... FROM (SELECT ... UNION ALL SELECT ...) AS temp_table GROUP BY col1, col2, ...。有时查询优化器能为GROUP BY找到比UNION的隐式去重更优的执行计划。 - 回答要点 3 (分步处理):对于海量数据的离线任务,可以先用
UNION ALL将数据导入一个临时表,然后为该临时表创建索引,再通过DELETE或INSERT INTO ... SELECT DISTINCT的方式进行去重,将步骤分解,更可控。
- 回答要点 1 (应用层去重):如果返回给应用层的数据量不大(例如几千到几万行),可以考虑使用
-
追问:除了
UNION,SQL 中还有哪些集合操作?它们的性能如何?- 回答要点:SQL 标准还定义了
INTERSECT(交集)和EXCEPT(差集,在 Oracle 中是MINUS)。INTERSECT:返回两个查询共有的行,并去重。EXCEPT:返回存在于第一个查询结果但不存在于第二个查询结果的行,并去重。- 性能:
INTERSECT和EXCEPT都需要找出重复或不存在的行,因此它们的实现机制与UNION类似,也涉及到排序或哈希操作,性能开销较大。它们同样有ALL版本(如INTERSECT ALL,EXCEPT ALL,虽然并非所有数据库都支持),性能会更好。值得注意的是,MySQL 在 8.0.31 才完整支持INTERSECT和EXCEPT,在此之前通常用(LEFT/RIGHT) JOIN或EXISTS/NOT EXISTS来模拟,性能视具体场景而定。
- 回答要点:SQL 标准还定义了
-
追问:如果
UNION的两个查询返回的列名不同,或者数据类型不完全一致,会发生什么?- 回答要点 1 (列名):最终结果集的列名由
UNION操作符前的第一个SELECT语句决定。后续SELECT语句的列名会被忽略。 - 回答要点 2 (数据类型):数据库会进行隐式类型转换,将所有查询的对应列转换为一个兼容的“超级”类型。例如,如果一列是
INT,另一列是VARCHAR(10),结果列的类型可能会被统一转换为VARCHAR(10)。这可能导致索引失效或精度丢失,因此最佳实践是确保UNION的各部分列类型完全一致。
- 回答要点 1 (列名):最终结果集的列名由