订阅更新

UNION ALL vs UNION,别再无脑用UNION了

SQL手册 第 16 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 为什么UNION慢?
  3. 什么时候才需要UNION?
  4. 实战用法:UNION ALL的5个妙用
  5. 面试注意事项
  6. 总结

先说结论

UNION ALL: 直接合并,不去重,性能好

UNION: 合并后去重,需要额外的排序/Hash操作,性能差

在实际工作中,90%的场景应该用UNION ALL。只有你明确需要去重的时候才用UNION。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

UNION和UNION ALL看起来就差一个关键词,但在大数据场景下性能差距可以有几十倍。面试官问这个问题,不是在考语法,而是在考你有没有性能意识。

为什么UNION慢?

UNION在合并数据后会做一次全局去重,底层实现相当于在结果集上做了一次DISTINCT或者GROUP BY。在Hive/Spark里,这意味着一次额外的Shuffle——所有数据要重新分发到不同节点做排序去重。

假设两个子查询各返回1亿行数据:

UNION ALL:直接拼接,返回2亿行,零额外开销

UNION:2亿行数据做Shuffle + 排序去重,可能要十几分钟

-- ❌ 不需要去重的场景用了UNION,白白多一次Shuffle
SELECT user_id, event_name, dt FROM app_event WHERE dt = '2025-01-01'
UNION
SELECT user_id, event_name, dt FROM web_event WHERE dt = '2025-01-01'
 
-- ✅ 用UNION ALL,快几十倍
SELECT user_id, event_name, dt FROM app_event WHERE dt = '2025-01-01'
UNION ALL
SELECT user_id, event_name, dt FROM web_event WHERE dt = '2025-01-01'

App事件和Web事件来自不同的表,数据本身不会重复,用UNION去重完全是浪费。

什么时候才需要UNION?

需要去重的场景其实很少,常见的有:

场景一:合并可能重叠的数据源

-- 两个渠道都可能记录同一个用户的注册
SELECT user_id, register_date FROM channel_a_register
UNION
SELECT user_id, register_date FROM channel_b_register

如果用户同时被两个渠道记录了,UNION会自动去重。但更好的做法是用UNION ALL + ROW_NUMBER:

SELECT user_id, register_date FROM (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY register_date) AS rn
  FROM (
    SELECT user_id, register_date FROM channel_a_register
    UNION ALL
    SELECT user_id, register_date FROM channel_b_register
  ) combined
) t
WHERE rn = 1

这种写法更灵活——你可以控制"保留哪一条"(比如保留最早的注册记录),而UNION只是简单去重,不能指定保留策略。

场景二:生成不重复的维度值列表

-- 获取所有出现过的渠道名(去重)
SELECT channel FROM app_event
UNION
SELECT channel FROM web_event

这个场景用UNION确实合理,但数据量通常很小(几十个渠道),性能不是问题。

实战用法:UNION ALL的5个妙用

用法一:列转行

DAY 3讲过,UNION ALL是列转行的通用写法:

SELECT name, '语文' AS subject, chinese AS score FROM score_wide
UNION ALL
SELECT name, '数学' AS subject, math AS score FROM score_wide
UNION ALL
SELECT name, '英语' AS subject, english AS score FROM score_wide

用法二:合并不同粒度的汇总

做报表经常需要在同一张表里输出明细和汇总:

-- 每个渠道的消耗 + 总计
SELECT channel, SUM(cost) AS total_cost
FROM ad_daily_cost
WHERE dt = '2025-01-01'
GROUP BY channel
 
UNION ALL
 
SELECT '总计' AS channel, SUM(cost) AS total_cost
FROM ad_daily_cost
WHERE dt = '2025-01-01'

这比用GROUPING SETS更直观。当然如果汇总维度很多,GROUPING SETS会更简洁:

SELECT
  COALESCE(channel, '总计') AS channel,
  COALESCE(region, '全部') AS region,
  SUM(cost) AS total_cost
FROM ad_daily_cost
WHERE dt = '2025-01-01'
GROUP BY GROUPING SETS (
  (channel, region),
  (channel),
  ()
)

用法三:分段处理不同逻辑

当不同条件需要完全不同的计算逻辑时,拆成多个查询再UNION ALL是最清晰的写法:

-- 活跃用户:最近7天有登录
SELECT user_id, '活跃' AS status, MAX(login_date) AS last_date
FROM user_login
WHERE login_date >= DATE_SUB(current_date(), 7)
GROUP BY user_id
 
UNION ALL
 
-- 沉默用户:7-30天没登录
SELECT user_id, '沉默' AS status, MAX(login_date) AS last_date
FROM user_login
GROUP BY user_id
HAVING MAX(login_date) BETWEEN DATE_SUB(current_date(), 30) AND DATE_SUB(current_date(), 8)
 
UNION ALL
 
-- 流失用户:超过30天没登录
SELECT user_id, '流失' AS status, MAX(login_date) AS last_date
FROM user_login
GROUP BY user_id
HAVING MAX(login_date) < DATE_SUB(current_date(), 30)

用法四:INSERT多分区

Hive中一次写入多个分区,可以用UNION ALL配合动态分区:

INSERT OVERWRITE TABLE result PARTITION(dt)
SELECT user_id, metric_value, '2025-01-01' AS dt
FROM source WHERE dt = '2025-01-01'
UNION ALL
SELECT user_id, metric_value, '2025-01-02' AS dt
FROM source WHERE dt = '2025-01-02'

用法五:补全缺失数据

DAY 7讲同比环比时提到的日历表补0,用的就是UNION ALL的思路:

-- 真实数据
SELECT dt, channel, cost FROM ad_daily_cost
 
UNION ALL
 
-- 补全没有数据的日期
SELECT d.dt, c.channel, 0 AS cost
FROM date_dim d
CROSS JOIN (SELECT DISTINCT channel FROM ad_daily_cost) c
WHERE NOT EXISTS (
  SELECT 1 FROM ad_daily_cost a
  WHERE a.dt = d.dt AND a.channel = c.channel
)

面试注意事项

1. UNION ALL的列数和类型必须一致

-- ❌ 列数不同会报错
SELECT user_id, user_name FROM table_a
UNION ALL
SELECT user_id FROM table_b
 
-- ❌ 类型不兼容可能报错或隐式转换
SELECT user_id, cost FROM table_a     -- cost是DECIMAL
UNION ALL
SELECT user_id, event_name FROM table_b -- event_name是STRING

2. ORDER BY只能写在最外层

-- ❌ 每个子查询单独排序无效(会被优化器忽略)
SELECT * FROM table_a ORDER BY dt
UNION ALL
SELECT * FROM table_b ORDER BY dt
 
-- ✅ 在最外层排序
SELECT * FROM (
  SELECT * FROM table_a
  UNION ALL
  SELECT * FROM table_b
) t
ORDER BY dt

3. UNION ALL可以超过两个

没有限制,可以UNION ALL十几个查询,但要注意可读性。如果超过5个,建议用CTE(WITH语句)拆分。

总结

对比UNION ALLUNION
-----------------------
是否去重否是
性能快(无额外操作)慢(需要Shuffle去重)
使用场景数据源不重叠明确需要去重
使用频率90%10%

一句话原则:默认用UNION ALL,除非你能说清楚为什么需要去重。

明天 DAY 17 聊SQL执行计划怎么看,EXPLAIN的输出很多人觉得"看不懂",但其实只要抓住几个关键信息,就能快速定位性能瓶颈。

你在工作中有没有因为用了UNION而导致任务变慢的经历?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器