【MySQL 笔记】复合查询(Compound Queries / Set Operations)详解
在 MySQL 中,“复合查询”一词在不同语境下有两种常见含义:
- 广义:任何把多个查询逻辑组合起来的查询(JOIN + 子查询 + 集合运算等)
- 狭义(最常指):集合运算(Set Operations),即用
UNION、INTERSECT、EXCEPT等把多个 SELECT 的结果集进行并、交、差操作
今天我们重点讲狭义的复合查询——集合运算(MySQL 8.0+ 已完整支持),这是面试和数据分析中非常高频的内容。
一、MySQL 集合运算符对比表(2026 年现状)
| 运算符 | 含义 | 是否去重 | MySQL 支持版本 | 结果特点 | 典型场景 |
|---|---|---|---|---|---|
UNION | 并集 | 是 | 所有版本 | 去重后合并 | 合并同类数据(如历史表+新表) |
UNION ALL | 并集(不去重) | 否 | 所有版本 | 保留所有重复行,性能最好 | 日志合并、报表数据累加 |
INTERSECT | 交集 | 是 | 8.0.31+ | 只保留两边都出现的行 | 找出同时满足 A 和 B 条件的数据 |
INTERSECT ALL | 交集(保留重复) | 否 | 8.0.31+ | 保留最小重复次数 | 极少用,精确重复计数场景 |
EXCEPT | 差集(A – B) | 是 | 8.0.31+ | 只保留出现在 A、不在 B 的行 | 找出只在 A 表有的记录(差异对比) |
EXCEPT ALL | 差集(不去重) | 否 | 8.0.31+ | 减去对应重复次数 | 精确多对多差集 |
注意:MySQL 8.0.31 之前只有 UNION / UNION ALL,INTERSECT 和 EXCEPT 是 8.0.31(2022 年 10 月)才正式加入的。
二、基本语法结构
SELECT column_list FROM table1 [WHERE ...]
UNION [ALL | DISTINCT]
SELECT column_list FROM table2 [WHERE ...]
[ORDER BY ...]
[LIMIT ...];
- 多个 SELECT 的列数必须相同
- 列的数据类型要兼容(可隐式转换)
- 列名以第一个 SELECT 为准
ORDER BY和LIMIT只能出现在最后(整个复合查询的末尾)
三、实战示例表(建议自己建表跑)
-- 用户表 A(老数据)
CREATE TABLE users_old (
id INT PRIMARY KEY,
name VARCHAR(20),
city VARCHAR(20)
);
INSERT INTO users_old VALUES
(1,'张三','北京'), (2,'李四','上海'), (3,'王五','广州'), (4,'赵六','北京');
-- 用户表 B(新数据)
CREATE TABLE users_new (
id INT PRIMARY KEY,
name VARCHAR(20),
city VARCHAR(20)
);
INSERT INTO users_new VALUES
(3,'王五','深圳'), (4,'赵六','北京'), (5,'孙七','成都'), (6,'周八','杭州');
四、各种复合查询演示
-- 1. UNION(去重并集)
SELECT name, city FROM users_old
UNION
SELECT name, city FROM users_new;
-- 结果:张三北京、李四上海、王五深圳、赵六北京、孙七成都、周八杭州
-- (王五和赵六只出现一次)
-- 2. UNION ALL(不去重,推荐性能更好)
SELECT name, city FROM users_old
UNION ALL
SELECT name, city FROM users_new;
-- 结果:8 行,王五和赵六各出现两次
-- 3. INTERSECT(交集,找出两表都有的用户)
SELECT name, city FROM users_old
INTERSECT
SELECT name, city FROM users_new;
-- 结果:赵六 北京(因为 city 也必须完全相同)
-- 如果只比 id,可先 SELECT id
-- 4. EXCEPT(差集:老表有、新表没有的)
SELECT name, city FROM users_old
EXCEPT
SELECT name, city FROM users_new;
-- 结果:张三 北京、李四 上海、王五 广州
-- 5. 复杂组合 + 排序(常用于分页报表)
(SELECT name, city, 'old' AS source FROM users_old)
UNION ALL
(SELECT name, city, 'new' AS source FROM users_new)
ORDER BY name
LIMIT 5;
-- 6. 找出“只在新表出现”的用户(新用户)
SELECT name, city FROM users_new
EXCEPT
SELECT name, city FROM users_old;
-- 结果:孙七 成都、周八 杭州
五、高频使用场景总结(面试+实战)
| 场景 | 推荐写法 | 为什么推荐 |
|---|---|---|
| 历史表 + 当月表 合并报表 | UNION ALL | 不想去重,数据量大时性能关键 |
| 全量用户列表(去重) | UNION | 天然去重 |
| 找出新增用户 | 新表 EXCEPT 老表 | 清晰、直观 |
| 找出共同活跃用户 | INTERSECT | 8.0.31+ 直接写,很优雅 |
| 数据对账 / 差异比对 | EXCEPT + UNION 组合 | 快速定位只在一边的记录 |
| 多数据源合并(日志、爬虫等) | UNION ALL + source 列 | 保留原始记录 + 标记来源 |
六、性能 & 注意事项(2026 年视角)
- UNION ALL > UNION:不去重性能远高于去重(去重需要排序+判重)
- 列数 & 类型必须一致:否则报错
The used SELECT statements have a different number of columns - ORDER BY 放最后:且只能用列序号或第一个 SELECT 的列别名
... UNION ... ORDER BY 1 DESC; -- 第一列
- 索引利用:每个 SELECT 单独优化,复合后整体优化有限
- 替代方案(老版本或性能敏感时):
- 交集 →
INNER JOIN或EXISTS - 差集 →
LEFT JOIN ... WHERE right.id IS NULL - 但 8.0.31+ 后,
INTERSECT/EXCEPT可读性更好
下一期预告建议:MySQL 子查询(Subquery)全家桶 + 相关子查询 vs 非相关 + 性能对比
有复合查询写法特别头疼的业务场景(比如多表合并去重、分库分表后聚合等),欢迎留言,我们一起分析~
祝大家复合查询写得又稳又快!📊