7.7 KiB
7.7 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-05-16 00:00 |
UNION / UNION ALL
概述
UNION 是 SQL 标准中的集合运算,用于将多个 SELECT 的结果纵向合并为一个结果集。核心应用场景包括:分表数据汇总、多源 Feed 流合并、以及需要排序去重的跨查询聚合。
基本语法
-- 两种形式
SELECT col1, col2 FROM table_a
UNION -- 去重(内部排序 + 去重)
SELECT col1, col2 FROM table_b;
SELECT col1, col2 FROM table_a
UNION ALL -- 不去重(直接拼接,性能更高)
SELECT col1, col2 FROM table_b;
UNION vs UNION ALL
| 特性 | UNION | UNION ALL |
|---|---|---|
| 去重 | ✅ 内部去重 | ❌ 保留所有行 |
| 性能 | 低(需排序去重) | 高(直接追加) |
| ORDER BY 位置 | 只能放在最后一个 SELECT | 同上 |
| 适用场景 | 需要唯一结果的合并 | 已知不重复的合并 |
flowchart LR
A1["数据源 A"] --> U
B1["数据源 B"] --> U
U{"UNION"} --> R1["全部数据去重后输出"]
U2{"UNION ALL"} --> R2["全部数据直接拼接"]
style R1 fill:#FF9F43,color:#000
style R2 fill:#00D866,color:#fff
[!TIP] 首选 UNION ALL 如果你能通过业务逻辑保证各部分结果不重复(比如按日期区间划分),一律用 UNION ALL。UNION 的去重操作需要 Sort/Dedup 阶段,在大结果集上是昂贵操作。
实战场景
场景一:多表同构合并
-- 日志系统按月分表:logs_202601, logs_202602, ..., logs_202605
SELECT * FROM logs_202601
UNION ALL
SELECT * FROM logs_202602
UNION ALL
SELECT * FROM logs_202603
UNION ALL
SELECT * FROM logs_202604
UNION ALL
SELECT * FROM logs_202605
WHERE status = 'error'
ORDER BY created_at DESC
LIMIT 50;
[!NOTE] 分表合并的注意事项
- 上述写法中,
WHERE / ORDER BY / LIMIT仅作用于最后一个 SELECT。前面的分表查询不做过滤,全部返回后再合并排序。- 如果每个分表数据量很大(百万级),建议用子查询保护每个表的局部 ORDER BY + LIMIT,先各取 Top-N 再全局排。这大幅减少中间结果集大小。
- UNION ALL 不保证顺序,最终 ORDER BY 必不可少。
场景二:多表分页合并(Feed 流)
-- 需求: 用户的动态 Feed 由关注的人和发布的文章混合组成, 按时间排序分页
-- ❌ 应用层先查再排: 至少两次 DB 往返 + 内存归并排序
-- ✅ UNION ALL 一次搞定
SELECT user_id AS source_id, content, 'follow' AS source_type, created_at
FROM follow_feed WHERE user_id = 42
UNION ALL
SELECT article_id AS source_id, summary AS content, 'article' AS source_type, published_at
FROM articles WHERE author_id = 42
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;
[!TIP] 何时用 UNION vs CASE WHEN?
- 数据来源是不同表或不同结构: UNION ALL 是不二之选
- 同表不同条件的聚合计数:
SUM(CASE WHEN ...)单次扫描更高效- 经验法则: UNION 的 SQL 可读性显著优于多个 OR 条件叠加时, 优先选 UNION
场景三:分页合并多个条件
-- 搜索商品:标题含关键词 或 描述含关键词
SELECT id, title, description, score_title
FROM products
WHERE title LIKE '%runners%'
UNION ALL
SELECT id, title, description, score_desc
FROM products
WHERE description LIKE '%runners%';
[!WARNING] UNION 的字段匹配规则
- 各 SELECT 的列数必须相同
- 各 SELECT 对应列的类型应尽量兼容(MySQL 会自动转换)
- ORDER BY 和 LIMIT 通常放在整个 UNION 的最后
- 列名取第一个 SELECT 的别名
- 注意: MySQL 不推荐在 UNION 的各个独立 SELECT 中使用 ORDER BY(结果可能被丢弃)
-- ✅ 正确
SELECT id, name FROM products WHERE price > 100
UNION ALL
SELECT id, name FROM products WHERE category = 'sale';
-- ❌ 错误:列数不一致
SELECT id, name FROM products
UNION ALL
SELECT id FROM discounts;
-- ❌ 错误:ORDER BY 放在中间(MySQL 可能忽略或报错)
SELECT id FROM table_a
ORDER BY id
UNION ALL
SELECT id FROM table_b;
UNION 进阶:ORDER BY 与 LIMIT 的行为
-- ⚠️ 问题:每个 SELECT 的 ORDER BY 在合并后无效
SELECT id FROM orders WHERE status = 'pending' ORDER BY created_at DESC
UNION ALL
SELECT id FROM orders WHERE status = 'shipped' ORDER BY created_at DESC;
-- 上面的 ORDER BY 基本被 MySQL 忽略
-- ✅ 正确解法:用子包装保护每个查询的排序
SELECT * FROM
(
SELECT id, created_at FROM orders WHERE status = 'pending'
ORDER BY created_at DESC LIMIT 50
) AS a
UNION ALL
SELECT * FROM
(
SELECT id, created_at FROM orders WHERE status = 'shipped'
ORDER BY created_at DESC LIMIT 50
) AS b
ORDER BY created_at DESC
LIMIT 20;
[!QUESTION] 为什么子查询能保护 ORDER BY? MySQL 优化器发现 UNION 外还有 ORDER BY + LIMIT 时, 会认为内部排序有用,从而保留它。 但官方文档并不保证这种行为——这是基于执行计划的经验结论, 生产环境务必确认。
UNION 的执行流程
flowchart TD
A["查询 1"] --> R1["结果集 1"]
B["查询 2"] --> R2["结果集 2"]
C["查询 N"] --> RN["结果集 N"]
R1 --> Temp["临时表 + 唯一索引<br/>UNION 有, UNION ALL 无"]
R2 --> Temp
RN --> Temp
Temp --> Dedup{"需要去重?"}
Dedup -->|是| Sort["排序 + 去重"]
Dedup -->|否| Direct["直接输出"]
Sort --> Output["最终结果集"]
Direct --> Output
style Dedup fill:#FF9F43,color:#000
style Sort fill:#EE5A24,color:#fff
与 JOIN 的选择
-- 场景:获取每个部门的员工总数 + 总监姓名
-- JOIN 方案(交叉维度,适合取不同列)
SELECT d.name, COUNT(e.id) AS emp_count, mgr.name AS manager
FROM departments d
LEFT JOIN employees e ON d.id = e.dept_id
LEFT JOIN employees mgr ON d.manager_id = mgr.id
GROUP BY d.id;
-- UNION 方案(平行维度,适合合并同类数据)
SELECT dept_id, 'total' AS metric, COUNT(*) AS value FROM employees GROUP BY dept_id
UNION ALL
SELECT dept_id, 'managers' AS metric, COUNT(*) AS value
FROM employees WHERE is_manager = 1 GROUP BY dept_id;
[!QUESTION] UNION 还是多个查询? 很多场景中 UNION 看起来方便,但背后可能有更好的解法:
- 应用层合并:在 Go/Java 中发两次查询然后合并数组(零 DB 压力)
- STORED PROCEDURE:存储过程中多次查询 + 临时表
- 视图:封装 UNION 逻辑供多次复用
核心原则:能不在数据库做的就不做。UNION 的代价是排序、去重、临时表。
性能对比:UNION vs 替代方案
| 方案 | DB 往返次数 | 临时表开销 | 排序开销 | 适用规模 |
|---|---|---|---|---|
| UNION ALL | 1 次 | 有(结果集缓冲) | 仅外层的 ORDER BY | 百万行级 |
| UNION(去重) | 1 次 | 有(唯一索引) | Sort + Dedup | 十万行以内 |
| 应用层 N 次查询 | N 次 | 无 | 内存归并 | 任意,受网络影响 |
| SUM(CASE WHEN) | 1 次 | 无 | 无 | 单表聚合计数 |
flowchart LR
A["数据源数量 > 1"] --> B{"能否用单次扫描解决?"}
B -->|是: 单表多条件| C["SUM CASE WHEN<br/>最优"]
B -->|否| D{"结果集是否已知不重复?"}
D -->|是| E["UNION ALL<br/>推荐"]
D -->|否| F["UNION<br/>去重"]
style C fill:#00D866,color:#fff
style E fill:#74C0FC,color:#000
style F fill:#FF9F43,color:#000
关联笔记
- hhs/MySQL/12-DQL SELECT 全解析 — SELECT 基础语法与 UNION 的结合使用
- hhs/MySQL/03-CRUD 操作 — DML 中的批量操作与 UNION 的互补关系