本页内容受版权保护 · 已添加水印 · 禁止任何形式转载
查询基础(SELECT / JOIN / 聚合 / 排序) · 集合运算

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 操作。

内部执行过程: 为了找出并删除重复行,数据库必须对合并后的整个结果集进行一次去重处理。这通常通过以下两种方式之一实现:

  1. 排序(Sort):将所有结果行放入一个临时空间,然后对所有行进行排序,最后遍历排序后的结果,只保留唯一的行。
  2. 哈希(Hash):将结果行逐一放入一个哈希表。在放入前,检查该行是否已存在于哈希表中。如果不存在,则放入哈希表并加入最终结果集;如果已存在,则跳过。

这两种方式都需要额外的 CPU 和内存资源,当数据量巨大时,临时空间可能不足,导致使用磁盘(filesort),性能会急剧下降。

2. UNION ALL

UNION ALL 操作符合并结果集,但它不进行任何去重操作,直接将第二个查询的结果追加到第一个查询结果的末尾。

内部执行过程: 这是一个非常直接的操作。数据库依次执行每个 SELECT 语句,并将结果流式地、不加处理地追加到最终结果集中。它不涉及行与行之间的比较,因此没有排序或哈希的开销。

示例与对比

假设我们有两张表 employees_cn(中国区员工)和 employees_us(美国区员工)。

employees_cn

idname
1Alice
2Bob

employees_us

idname
2Bob
3Charlie

UNION 查询:

sql
1SELECT id, name FROM employees_cn
2UNION
3SELECT id, name FROM employees_us;

结果(去重后):

idname
1Alice
2Bob
3Charlie

UNION ALL 查询:

sql
1SELECT id, name FROM employees_cn
2UNION ALL
3SELECT id, name FROM employees_us;

结果(直接合并):

idname
1Alice
2Bob
2Bob
3Charlie

性能差异根源:为什么 UNION ALL 更快?

UNION 的执行计划 ≈ UNION ALL 的执行计划 + 去重操作(排序或哈希)

  • CPU 开销:UNION 需要比较行数据来判断是否重复,消耗 CPU。UNION ALL 不需要。
  • 内存/磁盘 I/O 开销:UNION 需要一个临时工作区(内存或磁盘)来执行排序或建立哈希表。UNION ALL 直接输出结果,几乎没有额外内存开销。

因此,UNION ALL 的性能总是优于 UNION,除非结果集本身非常小,差异可以忽略不计。

常见低分回答

  1. 回答过于肤浅:“UNION 去重,UNION ALL 不去重”。这只说出了表面现象,没有解释背后的性能原理,无法体现深度。
  2. 断言 UNION 一定会排序:“UNION 会对结果进行排序,所以慢”。虽然排序是去重的常用方法,但数据库也可能使用哈希算法。这种不严谨的表述会显得知识面不够广。
  3. 滥用 UNION:在业务逻辑上可以保证结果集不会有重复的情况下,仍然使用 UNION。例如:SELECT id FROM users WHERE status = 'active' UNION SELECT id FROM users WHERE status = 'inactive'。因为一个用户的 status 不可能同时是 active 和 inactive,所以结果天然无重复,此处使用 UNION 是不必要的性能浪费,应使用 UNION ALL。
  4. 混淆 UNION 和 JOIN:说出“UNION 是把两个表横向拼起来”之类的错误描述。UNION 是纵向合并行,JOIN 是横向合并列。
  5. 忽略 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 查询,则没有这个步骤。
    sql
    1-- MySQL 8.0+
    2EXPLAIN FORMAT=TREE
    3SELECT id FROM employees_cn
    4UNION
    5SELECT id FROM employees_us;
    6
    7-- 输出可能包含 -> Table scan on <temporary> (cost=... rows=...)
    8-- -> Deduplicate rows
  • 大数据量场景:当合并上亿级别的数据行时,UNION 带来的 Using temporary 和 filesort 可能是灾难性的,它会消耗大量内存并可能将临时数据写入磁盘,导致查询时间从秒级恶化到分钟级甚至小时级。UNION ALL 则能保持流式处理,性能稳定。
  • 索引优化:UNION 或 UNION ALL 本身无法直接利用索引。但其包含的每个 SELECT 子查询都应该被独立优化。确保每个子查询的 WHERE 条件、JOIN 条件都能有效利用索引,是整个查询高性能的基础。

常见追问与延伸题目

  1. 追问:如果业务上必须去重,但 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 的方式进行去重,将步骤分解,更可控。
  2. 追问:除了 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 来模拟,性能视具体场景而定。
  3. 追问:如果 UNION 的两个查询返回的列名不同,或者数据类型不完全一致,会发生什么?

    • 回答要点 1 (列名):最终结果集的列名由 UNION 操作符前的第一个 SELECT 语句决定。后续 SELECT 语句的列名会被忽略。
    • 回答要点 2 (数据类型):数据库会进行隐式类型转换,将所有查询的对应列转换为一个兼容的“超级”类型。例如,如果一列是 INT,另一列是 VARCHAR(10),结果列的类型可能会被统一转换为 VARCHAR(10)。这可能导致索引失效或精度丢失,因此最佳实践是确保 UNION 的各部分列类型完全一致。