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

7.7 KiB
Raw Blame History

tags, create time
tags create time
MySQL
UNION
UNION ALL
集合运算
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

关联笔记