UNION 和 UNION ALL,你的 SQL 慢一倍,可能就是多写了一个all
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 就简单了:
直接把两个结果集首尾拼接在一起,挨个输出,不做任何额外处理。
说数字最清楚。
假设 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 的去重成本,随数据量增长是非线性的。
-- 很多人以为 UNION 自动帮他们去重,
-- 于是这么写:
SELECT user_id, login_date
FROM app_login_2025
UNION
SELECT user_id, login_date
FROM app_login_2024;问题在哪?
UNION 去重是整行完全相同才去掉。
如果同一用户在不同年份的登录日期不同,整行就不一样,
UNION 根本不会去重。
结果:用户重复出现,统计 DAU 反而偏高。
SELECT user_id, '2025' AS year
FROM users_2025
UNION
SELECT user_id, '2024' AS year
FROM users_2024;这个其实是对的,因为加了 year 列标签之后两边的行本来就不一样。
但很多人把 UNION 当成"合并同类项"的工具,
一旦忘记加区分列,去重逻辑就完全不是你以为的那样。
这是最让人崩溃的一种。
SELECT user_id FROM orders
UNION
SELECT user_id FROM users;结果数量比预期少。
原因很简单:
如果某个 user_id 同时出现在 orders 和 users 表里,
UNION 就会把它合并成一条。
这不是 bug,是设计逻辑。
但很多人根本不知道,直到报表数据莫名其妙变少。
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 实际执行顺序是:
意味着:排序的数据量 = A 表结果 + B 表结果
而不是分别排序再合并。
数据量大的话,这个全局排序会吃大量内存,可能触发临时文件。
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 是对的,不能省。
-- 查所有活跃用户(包括有订单的和已注册的)
SELECT user_id FROM orders
UNION
SELECT user_id FROM users;这里你确实希望去重:
同一个 user_id 只出现一次。
那就用 UNION,这是正确用法。
SELECT '大促活动' AS campaign, product_id, sales
FROM promotion_sales
UNION
SELECT '日常销售' AS campaign, product_id, sales
FROM daily_sales;加了一个 campaign 标签列,两边结果天然不同,
UNION 去重在这个场景下是合理的额外保障。
有些场景下,逻辑上明明知道不会有重复,
但 UNION 写起来比 UNION ALL + 额外逻辑更清晰。
这时候用 UNION 不算错,只是要知道性能代价。
以下场景,不要碰 UNION:
-- 2024 年用户和 2025 年用户,通常不会有交集
SELECT user_id, '2024' AS year
FROM users_2024
UNION ALL
SELECT user_id, '2025' AS year
FROM users_2025;-- 拉清单:每个用户每个渠道的活跃记录
SELECT user_id, channel, active_date
FROM app_channel_login
UNION ALL
SELECT user_id, channel, active_date
FROM web_channel_login;这里去重会丢失"同一个用户多个渠道"的信息,
只能用 UNION ALL。
-- 正确写法:先合并明细,再统一聚合
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 ALL 是默认选项,UNION 是例外。
不要因为 UNION "看起来更干净",就默认用它。
多写一个 ALL,省掉的可能是一整趟排序,和一张报表的等待时间。
下次写 SQL 之前,先问自己一句: 我真的需要去重吗?
这个问题,值得你多花三秒钟。
阅读原文:原文链接