MySQL合并查询结果集:UNION与UNION ALL的核心区别与选型指南

在数据库操作中,合并多个查询结果集是常见需求。面对UNION和UNION ALL这两个选项,很多开发者会凭直觉选择,但这背后其实是一场性能与准确性的权衡。简单来说,UNION会自动去重并排序,但性能开销大;而UNION ALL仅仅是将结果拼接起来,不去重也不排序,效率极高。两者的选择,完全取决于你的数据语义和性能需求,前提是字段的数量、顺序和类型必须严格一致。
UNION 会自动去重并排序,但代价是性能下降
直接使用UNION合并查询,数据库会在背后默默做两件“重”活儿:去重和排序。这个过程,相当于为每个子查询的结果自动加上了一个DISTINCT,然后再进行默认排序。听起来很省心?代价是性能。为了完成这些操作,数据库引擎需要将全部数据拉出来,创建临时表,然后排序并逐行比对。在MySQL的执行计划(EXPLAIN)里,你常会看到Using temporary和Using filesort这两个标志,一旦数据量达到百万级,查询延迟就会直线飙升。
- 去重是“严格比对”:数据库的去重逻辑并非智能跳过,而是暴力比较。字段类型、具体的值,甚至
NULL,都必须完全一致才会被判定为重复行。 - 排序规则可能出乎意料:最终的排序规则由字段的数据类型决定。例如,
VARCHAR字段会按照数据库的字符集校对规则(collation)来排序,这可能和你的业务预期不符。 - 子查询排序无效:需要特别注意,如果子查询内部包含了
ORDER BY,UNION操作会忽略它。任何最终结果的排序,都必须写在整个UNION语句的最外层。
UNION ALL 不去重也不排序,快但结果可能含重复
与UNION的“精致服务”相反,UNION ALL干的是纯粹的“体力活”:它简单地将两个结果集按顺序拼接起来,不做任何额外的检查和处理。不检查重复、不排序、不创建临时表,这使得它的I/O和CPU开销极低。性能差距有多大?TPC-H的测试数据显示,在百万行数据量下,UNION ALL的耗时大约在810毫秒,而UNION则可能长达9200毫秒,差距超过11倍。
- 结构一致性是铁律:两个
SELECT语句的字段数量、顺序和数据类型必须完全一致,否则会直接报错:ERROR 1222 (21000): The used SELECT statements ha ve a different number of columns。 - 注意隐式类型转换:虽然允许
NULL与非NULL值混合,但字段间细微的类型差异(比如VARCHAR(10)和VARCHAR(50))可能导致数据被意外截断,因为MySQL会按较短的字段长度来处理。 - 天然无重数据的首选:当你能确定两个子查询的结果集在业务上天然没有重叠时(例如查询不同日期的分区表),
UNION ALL就是唯一合理且高效的选择。
什么时候该用 UNION,什么时候必须用 UNION ALL
选择的关键在于明确你的需求:是要绝对的“唯一性”,还是极致的“速度”?别再凭感觉了,看看下面这些场景:
- 需要业务上的“唯一记录”,且无法保证数据源不重复:这时必须用
UNION。典型场景是合并用户主表和测试数据表,你需要确保同一个手机号不会因为来源不同而重复出现。 - 查询范围已被精确隔离:如果你查询的是分片表、历史表加当前表,或者已经用
WHERE条件精确划分了数据范围,那么UNION ALL是更佳选择。例如:SELECT * FROM order_2025_q4 UNION ALL SELECT * FROM order_2026_q1。 - 后续还需进行聚合或连接操作:如果合并后的结果还要进行
GROUP BY或JOIN,优先考虑UNION ALL。把去重的工作留给聚合阶段,不仅逻辑更可控,也能避免UNION的中间排序干扰索引的有效使用。 - 上线前的性能检查:在开发环境用小数据测试可能无所谓,但上线前务必用
EXPLAIN查看执行计划。如果出现了Using temporary,那基本就是UNION在拖慢查询,需要重新评估选型。
常见错误:字段不匹配、括号缺失、别名位置错
这些错误往往直接导致查询失败,而且报错信息有时并不直观:
- 列数不等:
SELECT id,name FROM t1 UNION SELECT id FROM t2会直接报错。数据库不会“自动补NULL”,列数必须严格相等。 - 子查询排序无效:像
(SELECT a FROM t1) UNION (SELECT b FROM t2 ORDER BY b)这样的写法,如果外层没有ORDER BY,子查询中的ORDER BY不仅无效,还可能被查询优化器忽略。 - 派生表别名缺失:
SELECT * FROM (SELECT id FROM t1 UNION ALL SELECT id FROM t2) AS tmp WHERE id > 100是正确的。但如果漏掉了AS tmp这个别名,就会触发Every derived table must ha ve its own alias错误。 - 混合使用的优先级:当
UNION和UNION ALL混合使用时,优先级是从左到右。不加括号很容易导致逻辑误判,稳妥的做法是用括号明确包裹每一个子查询。
在实际应用中,还有一个更隐蔽的陷阱:隐式类型转换导致的去重失效。例如,一个子查询返回INT类型,另一个返回DECIMAL(10,0),MySQL在执行UNION去重时,可能会将它们统一转换为浮点数进行比较。这会导致本该被去重的整数行,因为细微的精度差异而被当作不同值保留下来。这种问题不会引发报错,但会 silently 地返回错误的结果,通常只能通过人工核对样本数据才能发现。
