SQL中Union语句优化

Sabthever

当Union成为SQL中影响效率的核心问题,需优化Union语句提高SQL效率

Union语句优化

  1. 先问“要不要去重”——Union → Union ALL
  • Union 隐含 DISTINCT,需要对最终结果再做一次 排序-去重(内存或磁盘文件排序)。
  • Union ALL 只是简单拼接,省去排序,CPU & IO 直接减半甚至一个数量级。

适用:业务上允许出现重复行,或上游已保证唯一。
示例

1
2
3
4
5
6
7
8
9
-- 原句
SELECT order_id FROM orders_2023
UNION
SELECT order_id FROM orders_2024;

-- 优化
SELECT order_id FROM orders_2023
UNION ALL
SELECT order_id FROM orders_2024;

  1. 各分支单独“早过滤”——把 WHERE 下推到分支

错误写法(先合并再过滤):

1
2
3
4
SELECT * FROM (
SELECT * FROM orders_2023 UNION ALL SELECT * FROM orders_2024
) t
WHERE order_date >= '2024-01-01';

正确写法(每个分支自带 WHERE):

1
2
3
SELECT * FROM orders_2023 WHERE order_date >= '2024-01-01'
UNION ALL
SELECT * FROM orders_2024 WHERE order_date >= '2024-01-01';

效果:扫描分区量减少,可能直接走索引范围扫描。


  1. 只选需要的列——杜绝 SELECT *
  • 列越少,中间临时表越小,排序 / Hash 去重内存占用越低。
  • 对覆盖索引更友好(ICP、索引-only 扫描)。

  1. 让“各分支结果集”提前排序 → 利用 Merge Append

PostgreSQLMergeAppend 路径可以在 各分支已按同一列排序 的情况下,直接顺序归并,避免额外 Sort 节点。
示例

1
2
3
4
5
6
SELECT user_id, SUM(amount) FROM (
SELECT user_id, amount FROM orders_2023 ORDER BY user_id
UNION ALL
SELECT user_id, amount FROM orders_2024 ORDER BY user_id
) t
GROUP BY user_id;

Explain 可见Merge Append 而非 Sort + Append


  1. 给“去重列”建索引——让排序变索引顺序

如果 必须用 Union(去重),可对 去重涉及的列联合索引,优化器会优先选择 索引顺序扫描INDEX ORDER BY),减少外部排序。
MySQL 示例

1
2
CREATE INDEX uk_order_id ON orders_2023(order_id);
CREATE INDEX uk_order_id ON orders_2024(order_id);

Explain ExtraUsing temporary; Using filesortUsing index


  1. 分页场景“下推 + 限制”

OLAP 报表常用

1
2
3
4
(SELECT * FROM A ORDER BY id LIMIT 50000)
UNION ALL
(SELECT * FROM B ORDER BY id LIMIT 50000)
ORDER BY id LIMIT 50000;

原理:每个分支先取 Top-N,再全局归并,避免把全表数据拖进临时表。


  1. 并行执行——开启并行 Union 分支
  • PostgreSQLset max_parallel_workers_per_gather = 8; 并行 Append 已原生支持。
  • SQL ServerOPTION (MAXDOP 8)
  • MySQL 8.0:目前无并行 Union,但可 手动拆成多连接 + 应用层归并(高并发场景)。

  1. 物化视图 / 中间表——空间换时间

超大历史分区 + 频繁报表 场景,可 增量物化

1
2
3
4
5
6
7
8
CREATE MATERIALIZED VIEW mv_orders AS
SELECT order_id, user_id, amount, order_date
FROM orders_2023
UNION ALL
SELECT order_id, user_id, amount, order_date
FROM orders_2024;

-- 定时 REFRESH CONCURRENTLY

查询直接 SELECT ... FROM mv_orders,秒级响应。


  1. 改写为“分区表”——从根本上消灭 Union

MySQL 8.0 / PostgreSQL 12+ 均支持 原生分区(RANGE/LIST/HASH),优化器可直接 分区剪枝,再也无需手写 Union ALL。
示例(MySQL):

1
2
3
4
5
6
7
8
CREATE TABLE orders (
order_id BIGINT,
order_date DATE,
...
) PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025)
);

查询:

1
SELECT * FROM orders WHERE order_date >= '2024-01-01';

自动只扫 p2024 分区,与手写 Union ALL 等价,但 零维护成本


快速 checklist

检查项 预期结果
业务允许重复? 用 Union ALL
每个分支是否带 WHERE? 行数减半
是否 SELECT 只需要的列? 临时表更小
去重列是否有索引? 避免 filesort
能否改分区表? 彻底告别 Union

按表逐项验证,**90% 的 Union 性能问题可在一轮索引+Union ALL 改造后下降 50%~80%**。

实际案例

修改前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
select '实销完成率' AS type,
rank() OVER (ORDER BY rate DESC) AS rk, rate,
name,isJoin,
isWin
from (select f_actual_tm / f_target as rate -- 实销完成率
, f_combat_unit_code as name -- 作战单元
, sxwcl as isJoin
, sxwclsf as isWin
from tmp_combat
<if test="star != null and star != ''">
where f_star = concat(#{star}, '星')
</if>
) t

union

-- 国总 作战单元 实销增长率
-- 需要后端判断 如果sxzzl为空 排名为 - ,排名只排f_lx对应的PK榜类型非空的
select '实销增长率' AS type,
rank() OVER (ORDER BY rate DESC) AS rk, rate,
name,isJoin,
isWin
from (select if(#{isLast} = true, f_actual_tam / f_actual_lam - 1, f_actual_tm / f_actual_lm - 1) as rate -- 实销增长率
, f_combat_unit_code as name -- 作战单元
, sxzzl as isJoin
, sxzzlsf as isWin
from tmp_combat
<if test="star != null and star != ''">
where f_star = concat(#{star}, '星') -- 参数 星级
</if>
) t

union

-- 国总 作战单元 新能源增长率
-- 需要后端判断 如果nevzzl为空 排名为 - ,排名只排f_lx对应的PK榜类型非空的
select '新能源增长率' AS type,
rank() OVER (ORDER BY rate DESC) AS rk, rate,
name,isJoin,
isWin
from (select if(#{isLast} = true, f_actual_new_energy_tam / f_actual_new_energy_lam - 1,
f_actual_new_energy_tm / f_actual_new_energy_lm - 1) as rate -- 实销增长率
, f_combat_unit_code as name -- 作战单元
, nevzzl as isJoin
, nevzzlsf as isWin
from tmp_combat
<if test="star != null and star != ''">
where f_star = concat(#{star}, '星') -- 参数 星级
</if>
) t

主要优化点:

  1. 预计算和过滤:添加了tmp_combat_filteredCTE,提前计算所有三个指标并过滤数据,避免重复计算
  2. 使用UNION ALL:将UNION改为UNION ALL,避免去重开销
  3. 减少重复计算:在预处理阶段计算好所有需要的字段
  4. 保持结构清晰:将复杂的计算逻辑集中处理,使主查询更简洁

这样优化后,下面的UNION部分应该会快很多,因为:

  • 避免了重复的过滤条件计算
  • 减少了数据扫描次数
  • UNION ALL比UNION效率更高

tmp_area保持原样,因为它不影响性能。

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
WITH tmp_combat_filtered AS (
SELECT
f_combat_unit_code as name,
-- 实销完成率
f_actual_tm / f_target as rate_sxwcl,
sxwcl as isJoin_sxwcl,
sxwclsf as isWin_sxwcl,
-- 实销增长率
if(#{isLast} = true, f_actual_tam / f_actual_lam - 1, f_actual_tm / f_actual_lm - 1) as rate_sxzzl,
sxzzl as isJoin_sxzzl,
sxwclsf as isWin_sxzzl, -- 注意:保持原逻辑,实销增长率用sxwclsf作为isWin
-- 新能源增长率
if(#{isLast} = true, f_actual_new_energy_tam / f_actual_new_energy_lam - 1, f_actual_new_energy_tm / f_actual_new_energy_lm - 1) as rate_nevzzl,
nevzzl as isJoin_nevzzl,
nevzzlsf as isWin_nevzzl
FROM tmp_combat
<if test="star != null and star != ''">
WHERE f_star = concat(#{star}, '星')
</if>
),
type_list AS (
SELECT '实销完成率' as type
UNION ALL SELECT '实销增长率'
UNION ALL SELECT '新能源增长率'
),
ranked_data AS (
SELECT
t.name,
t.rate_sxwcl,
t.rate_sxzzl,
t.rate_nevzzl,
t.isJoin_sxwcl,
t.isJoin_sxzzl,
t.isJoin_nevzzl,
t.isWin_sxwcl,
t.isWin_sxzzl,
t.isWin_nevzzl,
RANK() OVER (ORDER BY t.rate_sxwcl DESC) as rk_sxwcl,
RANK() OVER (ORDER BY t.rate_sxzzl DESC) as rk_sxzzl,
RANK() OVER (ORDER BY t.rate_nevzzl DESC) as rk_nevzzl
FROM tmp_combat_filtered t
)
SELECT
tl.type,
CASE tl.type
WHEN '实销完成率' THEN rd.rk_sxwcl
WHEN '实销增长率' THEN rd.rk_sxzzl
WHEN '新能源增长率' THEN rd.rk_nevzzl
END as rk,
CASE tl.type
WHEN '实销完成率' THEN rd.rate_sxwcl
WHEN '实销增长率' THEN rd.rate_sxzzl
WHEN '新能源增长率' THEN rd.rate_nevzzl
END as rate,
rd.name,
CASE tl.type
WHEN '实销完成率' THEN rd.isJoin_sxwcl
WHEN '实销增长率' THEN rd.isJoin_sxzzl
WHEN '新能源增长率' THEN rd.isJoin_nevzzl
END as isJoin,
CASE tl.type
WHEN '实销完成率' THEN rd.isWin_sxwcl
WHEN '实销增长率' THEN rd.isWin_sxzzl
WHEN '新能源增长率' THEN rd.isWin_nevzzl
END as isWin
FROM ranked_data rd
CROSS JOIN type_list tl
WHERE
(tl.type = '实销完成率') OR
(tl.type = '实销增长率') OR
(tl.type = '新能源增长率')
ORDER BY tl.type, rk
匹配到优化点 说明
1. Union → Union ALL 把三条 UNION 改成 UNION ALL 再横向转列,实质彻底消灭了去重排序。
2. 早过滤(WHERE 下推) 星级条件 <if test="star..."> 在 CTE 第一步就生效,三张虚拟表共享同一份过滤结果。
3. 减少列宽度 原语句三次子查询都 SELECT *,改写后只取出后续要用的 10 余个指标列,不再拖整行。
4. 排序一次复用 原语句每条指标单独 RANK() OVER (ORDER BY rate DESC),共 3 次排序;改写后仅对三张“宽列”做一次窗口函数,三条指标同时拿到 rk_sxwcl / rk_sxzzl / rk_nevzzl,相当于 Merge Append 复用排序。
  • 标题: SQL中Union语句优化
  • 作者: Sabthever
  • 创建于 : 2025-11-12 10:37:53
  • 更新于 : 2025-11-13 16:40:42
  • 链接: https://sabthever.cn/2025/11/12/technology/bigdata/SQL中Union语句优化/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
目录
SQL中Union语句优化