GBase 8a
性能调优
文章

GBase 8a UNION ALL 与多路合并:少洗牌、少落盘的写法

发表于2026-06-02 13:19:1046次浏览15个评论

业务里经常要把多张结构相近的表「摞在一起」:按日分表的历史订单、多源入库的明细、或 ETL 中间结果汇总。第一反应是 UNION ALL,在单机库里往往够用;在 GBase 8a 的 MPP 里,合并方式会决定要不要 REDISTRIBUTE、会不会在 CN 上 Gather 超大结果集,性能可以差一个数量级。

本文说明 UNION ALL / UNION 在 8a 上的常见计划形态、与分布键的关系,以及更稳妥的合并写入路径。

一、UNION ALL 与 UNION:语义差一步,计划差一截

-- 分支 A:保留重复行,通常更利于优化器下推
SELECT id, amt, dt FROM orders_20260501
UNION ALL
SELECT id, amt, dt FROM orders_20260502;

-- 分支 B:去重,往往要多一轮 Sort/Distinct
SELECT id, amt, dt FROM orders_20260501
UNION
SELECT id, amt, dt FROM orders_20260502;
写法 语义 8a 上常见代价
UNION ALL 直接拼接 若两侧已同分布,可能本地 Append
UNION 去重合并 常伴随 Distinct/Sort,易落盘

原则:业务允许重复时一律优先 UNION ALL;必须去重时,评估是否能在各分片先 GROUP BY 再合并,而不是对大结果集做一次全局 DISTINCT。

二、分布键:合并前两表是否「同分布」

orders_20260501orders_20260502 都是 DISTRIBUTED BY (id),且 SELECT 的列与分布键兼容,优化器更可能让各 DN 本地拼接,Motion 较少。

反例:

  • 表 A 按 id HASH,表 B 按 dt HASH,却 UNION ALL 后按 id Join 大表 → 合并后往往要 REDISTRIBUTE 整坨数据。
  • 一侧是复制表、一侧是 HASH 表,合并键与分布键不一致 → 广播或洗牌二选一,都可能很贵。

合并前用 EXPLAIN 看计划(示例):

EXPLAIN
SELECT id, SUM(amt) FROM (
  SELECT id, amt FROM orders_20260501
  UNION ALL
  SELECT id, amt FROM orders_20260502
) t
GROUP BY id;

标注是否出现:

  1. 子查询上的 REDISTRIBUTE
  2. Append 是否在各 DN 本地完成
  3. GROUP BY id 能否 本地聚合(分布键 = id 时更优)

三、多表 UNION ALL 的正确姿势

1. 列类型与顺序对齐

MPP 对列宽、字符集、decimal 精度敏感。合并前统一:

  • 列顺序、别名一致;
  • 避免一侧 VARCHAR(20)、另一侧 VARCHAR(200) 隐式放大;
  • 日期类型一致,勿一侧 DATE 一侧 DATETIME 混用导致函数包裹,影响裁剪。

2. 先过滤再 UNION

SELECT id, amt FROM orders_20260501 WHERE dt = DATE '2026-06-01'
UNION ALL
SELECT id, amt FROM orders_20260502 WHERE dt = DATE '2026-06-01';

比「先 UNION 全表再 WHERE」少搬无数行。分表场景下,谓词下推到每个分支 是免费午餐。

3. 超多分支:考虑中间表或 gload

数十个 UNION ALL 嵌套会让计划复杂、编译变慢。更稳:

  1. 各源 gload 到同分布键的 staging 表;
  2. INSERT INTO target SELECT ... FROM branch_i 循环(注意事务大小);
  3. 最后对 target 做一次 ANALYZE

四、合并写入目标表:分布键要一次设计对

常见需求:多日分表合并进一张总表 orders_all,按 id 查询。

CREATE TABLE orders_all (
  id BIGINT,
  amt DECIMAL(18,2),
  dt DATE
) DISTRIBUTED BY ('id');

INSERT INTO orders_all
SELECT id, amt, dt FROM orders_20260501
UNION ALL
SELECT id, amt, dt FROM orders_20260502;

orders_alldt 分布而查询按 id,合并完成后业务 Join 仍会洗牌。建表时应对齐 最高频 Join/Group 键

大批量 INSERT ... SELECT ... UNION ALL 在高峰可能占满 IO;建议维护窗口、分批 INSERT,并监控 DN 的 tmp(参见大查询落盘文)。

五、去重合并的替代路径

必须全局去重时,可对比:

路径 AUNION(简单但计划可能重)

路径 B:分阶段

INSERT INTO staging SELECT id, amt, dt FROM ... UNION ALL ...;
-- staging 与目标表同分布键 id
INSERT INTO orders_dedup
SELECT id, amt, dt FROM staging
GROUP BY id, amt, dt;

路径 C:若去重键就是分布键,各分片 GROUP BY 后再 UNION ALL,减少全局 Distinct 数据量。

对每种路径跑 EXPLAIN,选 Motion 最少、预估行数最可信的一种(配合 ANALYZE)。

六、案例:分表 UNION 后 Join 变慢

场景:12 张按月分表,UNION ALL 后 Join 维表 users,合并子查询未下推过滤,单月查询却扫 12 表全量。

计划现象:巨大 Append → REDISTRIBUTE → Hash Join。

改法

  1. 应用层只 UNION 当月分表,或动态 SQL 只访问 orders_202606
  2. 维表小则改 REPLICATED(容量允许);
  3. 合并结果写入按 user_id 分布的中转表,再 Join。

改后应看到分支减少、REDISTRIBUTE 行数下降一个数量级。

小结

GBase 8a 上做多路合并,优先 UNION ALL + 分支下推 + 分布键对齐;慎用对大结果集的 UNION 去重。用 EXPLAIN 盯 Append 后是否跟大 REDISTRIBUTE,大批量合并考虑 staging/gload。把「合并前后计划、行数、耗时」记下来,团队以后看到 UNION 嵌套会先问分布键,而不是一味加索引。

评论

登录后才可以发表评论
GBase用户51829发表于 3个月前
GBase8a 合并多表优先 UNION ALL 规避全局去重排序开销,两表分布键一致可实现 DN 本地拼接减少数据重分布;多表合并要统一字段规格、提前下推过滤条件,配合 EXPLAIN 查看执行计划规避 REDISTRIBUTE 带来的性能损耗。
GBase用户51829发表于 3个月前
MPP 架构下 UNION 会额外做去重排序极易落地临时文件,能用 UNION ALL 就不用 UNION;分表合并关键看分布键,同分布键可本地 Append,分布规则不同大概率触发数据洗牌;实操遵循先过滤后合并、字段类型统一两大要点,EXPLAIN 校验执行计划是优化关键。
GBase用户50900发表于 3个月前
写得很好,学习了!
GBase用户51966发表于 3个月前
干货满满,感谢楼主分享。
一个老汉发表于 3个月前
分析得很透彻,学习了。
GBase用户51511发表于 3个月前
不错的文章,受益匪浅。
GBase用户51510发表于 3个月前
非常实用的内容,收藏了。
塔山发表于 3个月前
内容详实,值得收藏。
佛洛伊得发表于 3个月前
不错的文章,受益匪浅。
用户头像
GBase用户28017发表于 3个月前
谢谢分享。
GBase用户21182发表于 3个月前
很优秀,感谢分享。
GBase用户21182发表于 3个月前
很好
GBase用户51829发表于 3个月前
写的很棒,已收藏
GBase用户47954发表于 2个月前
感谢作者的精彩分享!
gbase0001发表于 2个月前
感谢分享,学习了