你的 SQL 慢一倍,可能就是多写了一个字
这是合理的,但如果把 ORDER BY 写在 UNION 的某个分支里——多写一个 ALL,省掉的可能是一整趟排序,和一张报表的等待时间。以后写 SQL,遇到 UNION 还是 UNION ALL,很多人写着写着,觉得 UNION "更规范"、"更干净",对所有列做排序,然后相邻行逐行比较,相同的只留一条。如果同一用户在不同年份的登录日期不同,整行就不一样,不要因为 UNION "看起来更干净",
两个查询,逻辑一模一样:
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,省掉的可能是一整趟排序,和一张报表的等待时间。
下次写 SQL 之前,先问自己一句: 我真的需要去重吗?
这个问题,值得你多花三秒钟。
更多推荐
所有评论(0)