474 lines
18 KiB
Markdown
474 lines
18 KiB
Markdown
|
|
---
|
|||
|
|
tags: [MySQL, UNION, UNION ALL, INTERSECT, EXCEPT, 集合运算]
|
|||
|
|
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 阶段,在大结果集上是昂贵操作。
|
|||
|
|
|
|||
|
|
## INTERSECT 与 EXCEPT(MySQL 8.0.22+)
|
|||
|
|
|
|||
|
|
MySQL 8.0.22 开始支持标准 SQL 的 `INTERSECT`(交集)和 `EXCEPT`(差集),补齐了集合运算的最后一块拼图。
|
|||
|
|
|
|||
|
|
### 基本语法
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- INTERSECT:取两个查询的交集(两边都有的行)
|
|||
|
|
SELECT city FROM customers_china
|
|||
|
|
INTERSECT
|
|||
|
|
SELECT city FROM customers_japan;
|
|||
|
|
-- 结果:只返回同时存在于两个结果集中的城市
|
|||
|
|
|
|||
|
|
-- EXCEPT:取差集(左边有、右边没有的行)
|
|||
|
|
SELECT city FROM customers_china
|
|||
|
|
EXCEPT
|
|||
|
|
SELECT city FROM customers_japan;
|
|||
|
|
-- 结果:返回在中国有但日本没有的城市
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!QUESTION] INTERSECT vs IN / EXISTS?
|
|||
|
|
> 两者在语义上等价,但 `INTERSECT` 更直观地表达了"取交集"的意图。实际执行中,Optimizer 通常会将 INTERSECT 转换为 Semi Join 或 Anti Join——因此性能差异不大,选择哪种取决于**可读性**。
|
|||
|
|
|
|||
|
|
### INTERSECT / EXCEPT vs UNION 行为对比
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
A["查询 A: {1,2,3}"] --> OP
|
|||
|
|
B["查询 B: {2,3,4}"] --> OP
|
|||
|
|
|
|||
|
|
OP --> R1["UNION: {1,2,3,4}"]
|
|||
|
|
OP --> R2["INTERSECT: {2,3}"]
|
|||
|
|
OP --> R3["EXCEPT: {1}"]
|
|||
|
|
|
|||
|
|
style R1 fill:#74C0FC,color:#000
|
|||
|
|
style R2 fill:#00D866,color:#fff
|
|||
|
|
style R3 fill:#FF9F43,color:#000
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| 操作 | 语义 | 去重 | 对应集合论 |
|
|||
|
|
|------|------|------|-----------|
|
|||
|
|
| `UNION` | A ∪ B | ✅ 默认去重 | 并集 |
|
|||
|
|
| `UNION ALL` | A ∪ B(含重复) | ❌ | 多重集并 |
|
|||
|
|
| `INTERSECT` | A ∩ B | ✅ 默认去重 | 交集 |
|
|||
|
|
| `EXCEPT` | A − B | ✅ 默认去重 | 差集 |
|
|||
|
|
|
|||
|
|
> [!NOTE] 默认行为:去重
|
|||
|
|
> INTERSECT 和 EXCEPT 默认都会去重(等价于 DISTINCT)。如果想去重保留所有行,目前 **MySQL 不支持 `INTERSECT ALL` / `EXCEPT ALL`**,需要通过 UNION ALL + 计数的方式模拟。
|
|||
|
|
|
|||
|
|
### 实用场景
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 场景一:找出"既买了 A 又买了 B"的用户(交集)
|
|||
|
|
SELECT user_id FROM orders WHERE product_id = 'A'
|
|||
|
|
INTERSECT
|
|||
|
|
SELECT user_id FROM orders WHERE product_id = 'B';
|
|||
|
|
|
|||
|
|
-- 等价的 EXISTS 写法(对比可读性)
|
|||
|
|
SELECT DISTINCT o1.user_id
|
|||
|
|
FROM orders o1
|
|||
|
|
WHERE o1.product_id = 'A'
|
|||
|
|
AND EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = o1.user_id AND o2.product_id = 'B');
|
|||
|
|
|
|||
|
|
-- 场景二:找出"注册了但从未下单"的用户(差集)
|
|||
|
|
SELECT id FROM users
|
|||
|
|
EXCEPT
|
|||
|
|
SELECT DISTINCT user_id FROM orders;
|
|||
|
|
|
|||
|
|
-- 等价的 NOT EXISTS 写法
|
|||
|
|
SELECT u.id FROM users u
|
|||
|
|
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!TIP] 何时选 INTERSECT/EXCEPT?
|
|||
|
|
> - **逻辑简单、需要取交集/差集**:`INTERSECT` / `EXCEPT` 语法最清晰
|
|||
|
|
> - **需要返回关联表的字段**:用 `EXISTS` / `NOT EXISTS` 或 JOIN,因为 INTERSECT/EXCEPT 只能操作整行或指定列
|
|||
|
|
> - **需要高性能**:大表场景下 EXPLAIN 确认执行计划——Optimizer 通常会转换为 Semi/Anti Join,性能与手写子查询一致
|
|||
|
|
|
|||
|
|
### 优先级与括号
|
|||
|
|
|
|||
|
|
当混合使用多个集合运算时,默认**从左到右**执行。需要用括号改变优先级:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 先 UNION 再 INTERSECT(左到右)
|
|||
|
|
SELECT id FROM t1
|
|||
|
|
UNION
|
|||
|
|
SELECT id FROM t2
|
|||
|
|
INTERSECT
|
|||
|
|
SELECT id FROM t3;
|
|||
|
|
-- 实际含义:t1 UNION (t2 INTERSECT t3) ← 错误!
|
|||
|
|
-- 实际执行:(t1 UNION t2) INTERSECT t3 ← 从左到右
|
|||
|
|
|
|||
|
|
-- ✅ 用括号明确意图
|
|||
|
|
(SELECT id FROM t1 UNION SELECT id FROM t2)
|
|||
|
|
INTERSECT
|
|||
|
|
SELECT id FROM t3;
|
|||
|
|
|
|||
|
|
-- 也可以用 ORDER BY + LIMIT 修饰每个子句(需括号)
|
|||
|
|
(SELECT id FROM t1 ORDER BY id LIMIT 10)
|
|||
|
|
INTERSECT
|
|||
|
|
(SELECT id FROM t2 ORDER BY id LIMIT 5);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!WARNING] 注意括号的必要性
|
|||
|
|
> 当 INTERSECT/EXCEPT 与 ORDER BY/LIMIT 混用时,不加括号可能产生歧义。**养成给每个子查询加括号的习惯**,避免优先级意外。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 实战场景
|
|||
|
|
|
|||
|
|
### 场景一:多表同构合并
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- ❌ 错误写法:WHERE / ORDER BY / LIMIT 只作用于最后一个 SELECT
|
|||
|
|
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' -- ⚠️ 只过滤 logs_202605!
|
|||
|
|
ORDER BY created_at DESC
|
|||
|
|
LIMIT 50;
|
|||
|
|
|
|||
|
|
-- ✅ 正确写法:如果需要对每个分表单独过滤,用子查询包裹
|
|||
|
|
SELECT * FROM
|
|||
|
|
(SELECT * FROM logs_202601 WHERE status = 'error') AS t1
|
|||
|
|
UNION ALL
|
|||
|
|
SELECT * FROM
|
|||
|
|
(SELECT * FROM logs_202602 WHERE status = 'error') AS t2
|
|||
|
|
UNION ALL
|
|||
|
|
SELECT * FROM
|
|||
|
|
(SELECT * FROM logs_202603 WHERE status = 'error') AS t3
|
|||
|
|
UNION ALL
|
|||
|
|
SELECT * FROM
|
|||
|
|
(SELECT * FROM logs_202604 WHERE status = 'error') AS t4
|
|||
|
|
UNION ALL
|
|||
|
|
SELECT * FROM
|
|||
|
|
(SELECT * FROM logs_202605 WHERE status = 'error') AS t5
|
|||
|
|
ORDER BY created_at DESC
|
|||
|
|
LIMIT 50;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!NOTE] 分表合并的注意事项
|
|||
|
|
> - `WHERE / ORDER BY / LIMIT` **仅作用于 UNION 中最后一个 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,
|
|||
|
|
3 AS rank_score -- 标题命中,权重最高
|
|||
|
|
FROM products
|
|||
|
|
WHERE title LIKE '%runners%'
|
|||
|
|
UNION ALL
|
|||
|
|
SELECT id, title, description,
|
|||
|
|
2 AS rank_score -- 描述命中,权重次之
|
|||
|
|
FROM products
|
|||
|
|
WHERE description LIKE '%runners%'
|
|||
|
|
AND title NOT LIKE '%runners%'; -- 排除已在上面出现的
|
|||
|
|
ORDER BY rank_score DESC, id;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!NOTE] 多条件搜索的 UNION ALL 策略
|
|||
|
|
> - 通过不同 SELECT 赋予不同权重(`rank_score`),合并后统一 `ORDER BY` 即可实现"加权排序"
|
|||
|
|
> - **注意去重边界**:第二个 SELECT 加 `NOT LIKE` 过滤可避免重复输出同一商品。如果无法在前端精确排重,可以考虑外层套一层 `DISTINCT`(代价是会退化为 UNION)或改用 `GROUP BY id`
|
|||
|
|
|
|||
|
|
```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
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 执行计划特征(EXPLAIN)
|
|||
|
|
|
|||
|
|
用 `EXPLAIN` 观察 UNION 的执行特征,能直观看到 Optimizer 的处理策略:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE users_a (id INT PRIMARY KEY, name VARCHAR(64), city_id INT);
|
|||
|
|
CREATE TABLE users_b (id INT PRIMARY KEY, name VARCHAR(64), city_id INT);
|
|||
|
|
|
|||
|
|
EXPLAIN SELECT * FROM users_a WHERE city_id = 10
|
|||
|
|
UNION ALL
|
|||
|
|
SELECT * FROM users_b WHERE city_id = 10;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
期望的 EXPLAIN 输出:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
+----+--------------+------------+------+---------------+------+---------+-------+-------+------+
|
|||
|
|
| id | select_type | table | type | key | extra| rows | ... | ref | |
|
|||
|
|
+----+--------------+------------+------+---------------+------+---------+-------+-------+------+
|
|||
|
|
| 1 | PRIMARY | users_a | ref | idx_city_id | NULL | 100 | ... | const | |
|
|||
|
|
| 2 | UNION | users_b | ref | idx_city_id | NULL | 100 | ... | const | |
|
|||
|
|
+----+--------------+------------+------+---------------+------+---------+-------+-------+------+
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
关键解读:
|
|||
|
|
|
|||
|
|
| 字段 | 含义 | UNION 中的表现 |
|
|||
|
|
|------|------|--------------|
|
|||
|
|
| **select_type** | 查询类型 | `PRIMARY`(第一个 SELECT) + 每个后续 SELECT 标记为 `UNION` |
|
|||
|
|
| **table** | 涉及的表 | 每个 UNION 分支独立显示一行 |
|
|||
|
|
| **Extra** | 附加信息 | UNION ALL 无额外信息;使用去重版 UNION 时 Extra 会出现 `Using temporary; Using filesort` |
|
|||
|
|
|
|||
|
|
> [!WARNING] UNION 去重的隐藏代价
|
|||
|
|
>
|
|||
|
|
> ```sql
|
|||
|
|
> -- 对比两个版本的 Extra
|
|||
|
|
> EXPLAIN SELECT * FROM users_a WHERE city_id = 10
|
|||
|
|
> UNION ALL -- Extra: (空)
|
|||
|
|
> SELECT * FROM users_b WHERE city_id = 10;
|
|||
|
|
>
|
|||
|
|
> EXPLAIN SELECT * FROM users_a WHERE city_id = 10
|
|||
|
|
> UNION -- Extra: Using temporary; Using filesort
|
|||
|
|
> SELECT * FROM users_b WHERE city_id = 10;
|
|||
|
|
> ```
|
|||
|
|
>
|
|||
|
|
> MySQL 内部会将所有 UNION 分支的结果收集到一个**临时表**中,然后对这个临时表做**全表扫描 + 文件排序**来完成去重。当结果集很大时,这就是性能瓶颈所在。
|
|||
|
|
>
|
|||
|
|
> 生产排查建议:如果 UNION 查询慢,先用 `EXPLAIN` 确认 Extra 是否包含 `Using temporary`;再用 `EXPLAIN ANALYZE`(MySQL 8.0.16+)查看各分支的实际行数和耗时。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 与 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
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 常见陷阱与排查
|
|||
|
|
|
|||
|
|
| 陷阱 | 症状 | 解法 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| **列数不匹配** | `The used SELECT statements have a different number of columns` | 确保每个 SELECT 的列数完全相同;用 `NULL` 补齐 |
|
|||
|
|
| **类型隐式转换** | UNION 中对应列类型不一致导致全表扫描或结果错误 | 手动 CAST 到一致类型(如 `CAST(0 AS CHAR)`) |
|
|||
|
|
| **ORDER BY 位置错误** | MySQL 忽略分支内的 ORDER BY 或报错 1221 | ORDER BY / LIMIT 放整个 UNION 的最后;需要保护内部排序请用派生表包装 |
|
|||
|
|
| **UNION ALL 产生重复行** | 业务期望唯一结果但实际有重复 | 检查是否真的有去重需求 — 有的话用 UNION;无法避免时用 DISTINCT/GROUP BY |
|
|||
|
|
| **分表 WHERE 漏写** | WHERE 只过滤最后一个分表,前面全部数据入库 | 每个分表子查询独立加 WHERE,或用视图封装 |
|
|||
|
|
| **分页错位** | OFFSET 导致合并后第一页显示第二页数据 | 先合并再整体 OFFSET,不能用局部 LIMIT + OFFSET 代替全局分页 |
|
|||
|
|
|
|||
|
|
### 实战排查 Checklist
|
|||
|
|
|
|||
|
|
遇到 UNION 相关慢查询或异常时,按以下顺序逐项检查:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
S["UNION 查询异常/慢"] --> C1{"EXPLAIN 看了吗?"}
|
|||
|
|
C1 -->|没看| STOP["🛑 先跑 EXPLAIN<br/>别盲猜,看 select_type 确认分支数"]
|
|||
|
|
C1 -->|看了| C2{"Extra 有 Using temporary 吗?"}
|
|||
|
|
|
|||
|
|
C2 -->|"是:UNION 去重"| D1["换 UNION ALL + 业务侧保证不重复"]
|
|||
|
|
C2 -->|"否"| C3{"列数/列类型一致吗?"}
|
|||
|
|
|
|||
|
|
C3 -->|"不一致"| D2["CAST 统一类型<br/>否则可能隐式转换导致索引失效"]
|
|||
|
|
C3 -->|"一致"| C4{"ORDER BY 在最终位置吗?"}
|
|||
|
|
|
|||
|
|
C4 -->|"否,放在中间"| D3["移动到最后<br/>或在每个分支外加子查询包裹"]
|
|||
|
|
C4 -->|"是"| C5{"WHERE 对所有分支都生效吗?"}
|
|||
|
|
|
|||
|
|
C5 -->|"否"| D4["每个分子查询独立加条件"]
|
|||
|
|
C5 -->|"是"| DONE["✅ SQL 结构正确,<br/>继续排查索引和数据分布"]
|
|||
|
|
|
|||
|
|
style STOP fill:#EE5A24,color:#fff
|
|||
|
|
style D1 fill:#FF9F43,color:#000
|
|||
|
|
style D2 fill:#FF9F43,color:#000
|
|||
|
|
style D3 fill:#FF9F43,color:#000
|
|||
|
|
style D4 fill:#EE5A24,color:#fff
|
|||
|
|
style DONE fill:#00D866,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 核心要点回顾
|
|||
|
|
|
|||
|
|
| 主题 | 一句话 |
|
|||
|
|
|------|--------|
|
|||
|
|
| UNION ALL vs UNION | 能用 ALL 就绝不用 UNION — 去重的临时表+文件排序代价远超想象 |
|
|||
|
|
| INTERSECT / EXCEPT | 8.0.22+ 原生支持交集与差集,语义比 IN/EXISTS 更直观,Optimizer 会转为 Semi/Anti Join |
|
|||
|
|
| ORDER BY/LIMIT 作用域 | 只在 UNION 最后一个 SELECT 生效;跨表排序必须外层包装子查询 |
|
|||
|
|
| 字段匹配规则 | 列数必须相同,类型应尽量兼容 — 列名取自第一个 SELECT |
|
|||
|
|
| Feed 流/多源合并 | UNION ALL 的经典场景,一次 DB 往返搞定多数据源混合 |
|
|||
|
|
| 分表汇总 | 同构分表最理想的聚合方式,但注意 WHERE 只对最后一个分支生效 |
|
|||
|
|
| 执行计划标识 | PRIMARY + UNION 双行 — Extra 中出现 `Using temporary` 就是去重开销的信号 |
|
|||
|
|
|
|||
|
|
> [!TIP] 核心心法
|
|||
|
|
> **UNION ALL 是你最好的朋友,UNION 是你的最后手段。**
|
|||
|
|
> 写 UNION 查询前问自己三个问题:① 结果集真的需要去重吗?② 能不能用 CASE WHEN 单表扫描代替?③ 如果拆成 N 次简单查询,应用层合并是不是更清晰?大部分时候答案会让你回到 UNION ALL。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[hhs/MySQL/02-SQL核心/08-DQL SELECT 全解析]] — SELECT 基础语法与 UNION 的结合使用
|
|||
|
|
- [[hhs/MySQL/02-SQL核心/07-DML 增删改]] — DML 中的批量操作与 UNION 的互补关系
|