--- 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["临时表 + 唯一索引
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
最优"] B -->|否| D{"结果集是否已知不重复?"} D -->|是| E["UNION ALL
推荐"] D -->|否| F["UNION
去重"] 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 的互补关系