UNION 和 UNION ALL,差了一个 ALL。这个 ALL 省掉的,不只是一个关键字——是一整趟排序去重的成本。
两个查询,逻辑一模一样:
SELECT user_id FROM orders UNION SELECT user_id FROM users;
SELECT user_id FROM orders UNION ALL SELECT user_id FROM users;
区别只有一个字。
但当数据量到了百万级,UNION 那条会慢到你怀疑人生。
很多人写着写着,觉得 UNION "更规范"、"更干净", 结果每次跑报表都比别人慢,还不知道为什么。
今天把这件事说透。
一、UNION 和 UNION ALL 本质差在哪
UNION:合并 + 去重
UNION 会做两件事:
1. 把两个结果集合并
2. 对合并后的结果做去重(DISTINCT)
去重怎么做的? 对所有列做排序,然后相邻行逐行比较,相同的只留一条。
这一排序,在数据量大的时候,是真的贵。
UNION ALL:只合并,不去重
UNION ALL 就简单了:
直接把两个结果集首尾拼接在一起,挨个输出,不做任何额外处理。
二、性能差距有多大
说数字最清楚。
假设 orders 有 200 万行,users 有 50 万行。
-- UNION:先去重再输出 SELECT user_id FROM orders UNION SELECT user_id FROM users; -- 执行大概 3~5 秒(取决于硬件) -- UNION ALL:直接拼接输出 SELECT user_id FROM orders UNION ALL SELECT user_id FROM users; -- 执行大概 1~2 秒(还是取决于硬件)
表面上是快一倍。 实际场景里,如果列更多、数据更碎,差距可以更大。
核心原因就一句:
UNION 的去重成本,随数据量增长是非线性的。
三、五个最常见的误用场景
误用 1:查多个表的同期用户,以为自己在"去重"
-- 很多人以为 UNION 自动帮他们去重, -- 于是这么写: SELECT user_id, login_date FROM app_login_2025 UNION SELECT user_id, login_date FROM app_login_2024;
问题在哪?
UNION 去重是整行完全相同才去掉。 如果同一用户在不同年份的登录日期不同,整行就不一样, UNION 根本不会去重。
结果:用户重复出现,统计 DAU 反而偏高。
误用 2:以为 UNION 会自动选"更好"的数据
SELECT user_id, '2025' AS year FROM users_2025 UNION SELECT user_id, '2024' AS year FROM users_2024;
这个其实是对的,因为加了 year 列标签之后两边的行本来就不一样。
但很多人把 UNION 当成"合并同类项"的工具, 一旦忘记加区分列,去重逻辑就完全不是你以为的那样。
误用 3:UNION 后发现结果变少了,百思不得其解
这是最让人崩溃的一种。
SELECT user_id FROM orders UNION SELECT user_id FROM users;
结果数量比预期少。
原因很简单: 如果某个 user_id 同时出现在 orders 和 users 表里, UNION 就会把它合并成一条。
这不是 bug,是设计逻辑。 但很多人根本不知道,直到报表数据莫名其妙变少。
误用 4:UNION 搭配 ORDER BY,性能灾难
SELECT user_id, order_time FROM orders WHERE order_time >= '2025-01-01' UNION SELECT user_id, login_time FROM users WHERE login_time >= '2025-01-01' ORDER BY user_id;
这条 SQL 实际执行顺序是:
1. 分别查两个表
2. 合并所有结果
3. 对整个合并结果做全局排序
意味着:排序的数据量 = A 表结果 + B 表结果 而不是分别排序再合并。
数据量大的话,这个全局排序会吃大量内存,可能触发临时文件。
误用 5:嵌套子查询里用 UNION,不知道外层 ORDER BY 对谁生效
SELECT * FROM ( SELECT user_id, 'order' AS src FROM orders UNION ALL SELECT user_id, 'user' AS src FROM users ) t ORDER BY user_id;
这里有个细节:
子查询里的 UNION ALL 先执行, 外层的 ORDER BY 是对整个子查询结果排序。
这是合理的,但如果把 ORDER BY 写在 UNION 的某个分支里—— 对不起,外层 ORDER BY 依然对整表生效, 子查询里的 ORDER BY 基本被忽略(取决于数据库实现)。
四、什么场景必须用 UNION,不能用 UNION ALL
有些场景,UNION 是对的,不能省。
场景 1:需要全局去重的结果集
-- 查所有活跃用户(包括有订单的和已注册的) SELECT user_id FROM orders UNION SELECT user_id FROM users;
这里你确实希望去重: 同一个 user_id 只出现一次。 那就用 UNION,这是正确用法。
场景 2:需要合并多个相关但不同的来源
SELECT '大促活动' AS campaign, product_id, sales FROM promotion_sales UNION SELECT '日常销售' AS campaign, product_id, sales FROM daily_sales;
加了一个 campaign 标签列,两边结果天然不同, UNION 去重在这个场景下是合理的额外保障。
场景 3:为了代码简洁,牺牲一点性能
有些场景下,逻辑上明明知道不会有重复, 但 UNION 写起来比 UNION ALL + 额外逻辑更清晰。
这时候用 UNION 不算错,只是要知道性能代价。
五、什么场景坚决用 UNION ALL
以下场景,不要碰 UNION:
场景 1:已知两表数据完全不重叠
-- 2024 年用户和 2025 年用户,通常不会有交集 SELECT user_id, '2024' AS year FROM users_2024 UNION ALL SELECT user_id, '2025' AS year FROM users_2025;
场景 2:需要保留所有明细,逐行分析
-- 拉清单:每个用户每个渠道的活跃记录 SELECT user_id, channel, active_date FROM app_channel_login UNION ALL SELECT user_id, channel, active_date FROM web_channel_login;
这里去重会丢失"同一个用户多个渠道"的信息, 只能用 UNION ALL。
场景 3:UNION ALL + GROUP BY,比 UNION 后聚合更高效
-- 正确写法:先合并明细,再统一聚合 SELECT user_id, COUNT(*) AS cnt FROM ( SELECT user_id FROM orders UNION ALL SELECT user_id FROM returns ) t GROUP BY user_id;
注意:这里子查询用 UNION ALL, 外层 GROUP BY 一次性搞定所有统计。
如果反过来:
-- 错误思路:两个聚合再 UNION SELECT user_id, SUM(order_cnt) AS total_cnt FROM ( SELECT user_id, COUNT(*) AS order_cnt FROM orders GROUP BY user_id UNION SELECT user_id, COUNT(*) AS order_cnt FROM returns GROUP BY user_id ) x GROUP BY user_id;
这个写法又慢又绕, 因为你做了两次聚合再合并,不如第一种。
六、一句话判断原则
以后写 SQL,遇到 UNION 还是 UNION ALL, 先用这个判断:
这两个结果集,有没有可能是同一个 user_id / 同一行?
• 有可能是 → 用 UNION(需要去重)
• 绝对不可能是 → 用 UNION ALL(直接拼接)
• 不确定 → 先想清楚,再决定
七、最后总结
UNION
UNION ALL
去重
✅ 会去重
❌ 不去重
性能
慢(需排序)
快(直接拼接)
结果行数
≤ 两表之和
= 两表之和
NULL 处理
NULL = NULL 不成立,去重逻辑复杂
简单拼接
适用场景
确实需要去重
已知无重复,或需保留所有明细
记住:UNION ALL 是默认选项,UNION 是例外。
不要因为 UNION "看起来更干净",就默认用它。 多写一个 ALL,省掉的可能是一整趟排序,和一张报表的等待时间。