This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/15-UNION 与集合运算.md
T
2026-05-17 00:06:11 +08:00

239 lines
7.7 KiB
Markdown

---
tags: [MySQL, UNION, UNION ALL, 集合运算]
create time: 2026-05-16 00:00
---
# UNION / UNION ALL
## 概述
UNION 是 SQL 标准中的集合运算,用于将多个 SELECT 的结果纵向合并为一个结果集。核心应用场景包括:**分表数据汇总**、**多源 Feed 流合并**、以及**需要排序去重的跨查询聚合**。
## 基本语法
```sql
-- 两种形式
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 | 同上 |
| **适用场景** | 需要唯一结果的合并 | 已知不重复的合并 |
```mermaid
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 阶段,在大结果集上是昂贵操作。
## 实战场景
### 场景一:多表同构合并
```sql
-- 日志系统按月分表: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 流)
```sql
-- 需求: 用户的动态 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
### 场景三:分页合并多个条件
```sql
-- 搜索商品:标题含关键词 或 描述含关键词
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(结果可能被丢弃)
```sql
-- ✅ 正确
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 的行为
```sql
-- ⚠️ 问题:每个 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 的执行流程
```mermaid
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 的选择
```sql
-- 场景:获取每个部门的员工总数 + 总监姓名
-- 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 次 | 无 | 无 | 单表聚合计数 |
```mermaid
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 的互补关系