GBase 8a UNION 和 UNION ALL 的使用边界
我最近看资料和整理报表链路时,越来越觉得 GBase 8a 里很多"结果总量对不上"的问题,并不在 join,也不在 group by,而是出在 UNION 和 UNION ALL 的使用边界上。
尤其是多来源数据拼接、主题层宽表汇总、阶段结果合并这些场景,只要没先想清楚到底要不要去重,后面口径偏差就很容易慢慢放大。
现场里很常见的情况是:
开发图省事直接用 union 把两个结果拼起来,后来业务发现某些本该重复保留的记录被去掉了;还有一种反过来,大家默认用了 union all,结果同一批数据从两条链路同时进来,最后总量翻倍。
这类问题最麻烦的地方在于,SQL 完全能跑,结果也不像错得很离谱,但口径会越往下越难解释。
我自己理解下来,这条线更接近集合语义和结果口径管理 。
如果不先明确"这两批数据是互斥的、可重复的,还是需要按业务键去重",后面再去追报表差异,成本会很高。
先把 UNION 和 UNION ALL 的业务语义分开
从我自己的理解看,这两个写法最大的区别不是语法,而是你对重复记录的态度。
| 写法 | 我自己的理解 | 适合场景 | 主要风险 |
|---|---|---|---|
| UNION | 合并后去重 | 两边结果逻辑上可能重复,且确实只保留一份 | 误删本该保留的重复记录 |
| UNION ALL | 合并后不去重 | 两边结果本来就是独立贡献 | 重复数据直接累加 |
真正到现场时,我自己更关注的是:
重复记录到底是不是业务意义上的重复。
现场里常见的几种误判
- 把"值一样"误当成"业务上就是重复"。
- 以为两条链路互斥,实际上有重叠。
- 以为去重应该交给
union,没有先定义业务主键。 - 明明只想拼接明细,却用了
union把同值明细吞掉。 - 明明应该在主键层去重,却直接在整行层面去重。
这些误判的共同点在于:没有先定义什么才算一条该保留的记录。
一个更接近现场的例子
业务需要合并 APP 和 H5 两个渠道的下单数据,原始表结构一致:
sql
create table stg_order_app (
order_id bigint,
user_id bigint,
pay_amt decimal(18,2)
);
create table stg_order_h5 (
order_id bigint,
user_id bigint,
pay_amt decimal(18,2)
);
如果直接写:
sql
select * from stg_order_app
union
select * from stg_order_h5;
看起来很自然,但这里隐含了一个非常强的前提:
只要两边整行一样,就只保留一条。
可真正落到现场时,你要先问清楚:
- 同一个
order_id会不会真的从两条链路重复进入? - 如果两边记录整行相同,业务是否确实只算一单?
- 如果同一个订单在两个来源里金额一致、用户一致,是重复采集还是不同业务事件?
很多时候,这些问题没有先说清楚,union 就已经把结果口径先定死了。
我实际排查时一般怎么判断该用哪个
第一种:明确两边互斥,优先考虑 UNION ALL
如果两条链路业务上就是独立来源,且不应该互相吞记录,我自己更倾向于先用 union all。
至少它不会替你偷偷做去重。
第二种:怀疑有重复,但不清楚重复规则,不要直接上 UNION
这时我更愿意先把数据拼起来,再按业务键判断重复,而不是直接让数据库按整行去重。
sql
select *
from (
select order_id, user_id, pay_amt, 'APP' as src from stg_order_app
union all
select order_id, user_id, pay_amt, 'H5' as src from stg_order_h5
) t;
然后再看业务键分布:
sql
select
order_id,
count(*) as dup_cnt
from (
select order_id from stg_order_app
union all
select order_id from stg_order_h5
) t
group by order_id
having count(*) > 1;
这一步我自己特别看重,因为它能把"重复"从模糊感受变成可验证的事实。
UNION 最容易带来的几个偏差
偏差一:整行相同就被吞掉,但业务上本该保留
比如两个来源恰好生成了完全相同的一行明细,union 会只保留一份。
如果业务本来要看的是事件量,而不是去重后的订单量,这就有偏差。
偏差二:以为在按主键去重,实际是在按整行去重
这点我现场里见过很多次。
业务说"订单去重",技术却直接用了 union。
但 union 去的是整行,不是你脑子里的订单主键。
偏差三:后续再做聚合时,已经很难还原原始贡献
一旦在前面被 union 去掉了,后面很难知道到底吞掉了哪些记录。
我自己更倾向的一套写法
如果业务规则还没完全坐实,我一般先保留原始贡献,再显式做业务去重。
sql
create table stg_order_all as
select order_id, user_id, pay_amt, 'APP' as src from stg_order_app
union all
select order_id, user_id, pay_amt, 'H5' as src from stg_order_h5;
然后按业务主键判断:
sql
select
order_id,
count(*) as rec_cnt
from stg_order_all
group by order_id
having count(*) > 1;
如果最终业务确定"同一个 order_id 只保留一份",那我更愿意在主键层显式处理,而不是直接依赖 union 的整行去重语义。
一个简单的对照表
| 业务问题 | 我更倾向的写法 | 原因 |
|---|---|---|
| 两边明确互斥 | UNION ALL | 不额外吞记录 |
| 两边可能重叠,但规则未定 | 先 UNION ALL 再分析 | 先保留现场信息 |
| 需要按业务主键去重 | 先拼接再按主键处理 | 语义更清楚 |
| 只关心整行唯一值 | UNION | 适合整行集合语义 |
一个批检查脚本示意
bash
#!/bin/bash
DBHOST=192.0.2.115
DBPORT=5258
DBNAME=dw_merge
DBUSER=merge_user
LOGDIR=/data/gbase/log/union_check
DAYSTR=$(date +%F)
mkdir -p "${LOGDIR}"
gccli -h ${DBHOST} -P ${DBPORT} -u ${DBUSER} ${DBNAME} <<'SQL' >> "${LOGDIR}/union_check_${DAYSTR}.log" 2>&1
select count(*) as app_cnt from stg_order_app;
select count(*) as h5_cnt from stg_order_h5;
select count(*) as union_cnt
from (
select order_id, user_id, pay_amt from stg_order_app
union
select order_id, user_id, pay_amt from stg_order_h5
) t;
select count(*) as union_all_cnt
from (
select order_id, user_id, pay_amt from stg_order_app
union all
select order_id, user_id, pay_amt from stg_order_h5
) t;
SQL
我自己更关注的不是哪种写法看起来更短,而是它是不是准确表达了业务对重复记录的态度。
结尾
我最近回头看 GBase 8a 里这类问题时,一个很明显的感受是:
union 和 union all 最大的差别,不是性能层面的争论,而是你到底把重复记录当成什么。
真正落到现场时,先把业务重复、整行重复和主键重复分开,再决定用哪一种写法,通常能比事后追报表偏差省很多时间。
参考资料
text
[1] GBase 社区个人中心
https://www.gbase.cn/community/user/46723
[2] GBase 8a 社区优质文章区
https://www.gbase.cn/community/section/11
[3] GBase 8a MPP Cluster SQL 参考手册
https://www.gbase.cn/community/post/1772
[4] GBase 8a
https://www.gbase.cn/community/section/11