239 lines
7.7 KiB
Markdown
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 的互补关系
|