SQL: 集合运算
1. 🌍 生活类比
想象你有两堆名片:
- UNION — 把两堆名片合在一起,重复的只留一张
- UNION ALL — 把两堆名片全部合在一起,重复的也保留
- INTERSECT — 只挑出两堆中都有的人
- EXCEPT — 从第一堆中去掉第二堆也有的人
集合运算就是对两个查询结果集进行"合并、取交、取差"的操作。
2. 🎯 核心概念
(1) UNION — 去重合并
将两个结果集合并,自动去除重复行。
-- 合并员工姓名和部门名称,自动去重
SELECT name FROM employees
UNION
SELECT name FROM departments;
(2) UNION ALL — 不去重合并
将两个结果集合并,保留所有重复行。性能更好,因为不需要去重。
-- 合并员工姓名和部门名称,保留所有行
SELECT name FROM employees
UNION ALL
SELECT name FROM departments;
(3) INTERSECT — 交集
返回两个结果集中都存在的行。
-- 找出既是员工姓名又在部门名称中出现的字符串(演示用法)
SELECT name FROM employees
INTERSECT
SELECT name FROM departments;
(4) EXCEPT / MINUS — 差集
返回在第一个结果集中存在、但第二个结果集中不存在的行。
-- EXCEPT(SQL Server、PostgreSQL、SQLite)
SELECT name FROM employees
EXCEPT
SELECT name FROM departments;
-- MINUS(Oracle)
SELECT name FROM employees
MINUS
SELECT name FROM departments;
(5) 列匹配规则
使用集合运算时,两个 SELECT 必须满足:
| 规则 | 说明 |
|---|---|
| 列数相同 | 两个 SELECT 的列数必须一致 |
| 类型兼容 | 对应列的数据类型必须兼容 |
| ORDER BY 放最后 | 只能在整个语句末尾使用一次 |
-- 正确:两列,类型匹配
SELECT name, salary FROM employees
UNION
SELECT name, location FROM departments;
-- 错误:列数不同
SELECT name, salary FROM employees
UNION
SELECT name; -- ❌ 列数不匹配
(6) 💡 何时使用哪种运算
| 运算 | 场景 | 是否去重 | 性能 |
|---|---|---|---|
| UNION | 需要合并且去重 | ✅ 是 | 较慢 |
| UNION ALL | 合并不需要去重 | ❌ 否 | 较快 |
| INTERSECT | 找共同部分 | ✅ 是 | 中等 |
| EXCEPT | 找差异部分 | ✅ 是 | 中等 |
UNION ALL,性能更好。
3. 📝 基本语法
-- UNION 语法
SELECT column1, column2 FROM table1
UNION
SELECT column1, column2 FROM table2
[ORDER BY column1];
-- UNION ALL 语法
SELECT column1, column2 FROM table1
UNION ALL
SELECT column1, column2 FROM table2;
-- INTERSECT 语法
SELECT column1, column2 FROM table1
INTERSECT
SELECT column1, column2 FROM table2;
-- EXCEPT 语法
SELECT column1, column2 FROM table1
EXCEPT
SELECT column1, column2 FROM table2;
- 集合运算中,列名以第一个 SELECT 的列名为准
- ORDER BY 只能出现在最后,且通常按列序号排序
- 每个 SELECT 内可以有自己的 WHERE、GROUP BY
4. 📌 示例
▶ 示例:查询所有人员的姓名来源
合并员工姓名和部门名称到一个结果集中。
-- 查看所有"名称"来源:员工 + 部门
SELECT name AS 名称, '员工' AS 来源
FROM employees
UNION ALL
SELECT name, '部门'
FROM departments
ORDER BY 来源;
说明:添加一个常量列标记数据来源,便于区分。
▶ 示例:找出只在员工表中出现的姓名
-- 员工姓名 减去 部门名称(找出仅员工表中有的)
SELECT name FROM employees
EXCEPT
SELECT name FROM departments;
思路:用 EXCEPT 做差集,快速找出"只在 employees 表中出现、departments 表中没有"的姓名。
5. 🎬 场景演练
(1) 场景一:合并多渠道客户名单
公司有线上和线下两个客户表,需要合并去重后生成完整客户名录。
-- 线上客户
SELECT customer_name, email, '线上' AS channel
FROM online_customers
UNION
-- 线下客户
SELECT customer_name, email, '线下'
FROM offline_customers
ORDER BY customer_name;
要点:用 UNION 而非 UNION ALL,自动去除重复客户。
(2) 场景二:对比两个月的销售差异
找出本月有销售但上月没有销售的产品。
-- 本月销售的产品
SELECT product_id FROM orders
WHERE order_date >= '2026-06-01'
EXCEPT
-- 上月销售的产品
SELECT product_id FROM orders
WHERE order_date >= '2026-05-01'
AND order_date < '2026-06-01';
要点:EXCEPT 天然适合"找差异"的场景。
❓ 常见问题
Q:UNION 和 UNION ALL 什么时候该用哪个? A: 如果不需要去重,优先用
UNION ALL,因为它不需要额外的去重步骤,性能更好。只有确定需要去重时才用UNION。
Q:集合运算中列名不一致会报错吗? A: 不会,结果集的列名以第一个
SELECT的列名为准。但列数和对应列的数据类型必须一致。
Q:INTERSECT 和 JOIN 有什么区别? A:
INTERSECT是按整行去匹配交集,而JOIN是按指定条件关联。如果需要"找完全相同的行"用INTERSECT,需要"按某个字段关联不同表的列"用JOIN。
Q:MySQL 支持 INTERSECT 和 EXCEPT 吗? A: MySQL 8.0 起支持
INTERSECT和EXCEPT。如果使用更早版本,需要用INNER JOIN和NOT EXISTS/LEFT JOIN ... IS NULL来模拟。
📖 小节
| 运算 | 作用 | 是否去重 |
|---|---|---|
| UNION | 合并两个结果集 | ✅ |
| UNION ALL | 合并两个结果集(保留重复) | ❌ |
| INTERSECT | 取两个结果集的交集 | ✅ |
| EXCEPT | 取两个结果集的差集 | ✅ |
- 列数和类型必须匹配
ORDER BY只能放在语句最后- 确定无重复时优先使用
UNION ALL提升性能
📝 作业
- 使用
UNION合并employees表中工资高于 8000 的员工姓名和departments表中位置为"北京"的部门名称。 - 使用
INTERSECT找出employees和departments表中都存在的名称。 - 使用
EXCEPT找出有员工但没有对应部门的部门名称(即 employees.department_id 引用不到 departments.id 的)。 - 思考:如果两个查询结果的列类型不完全一致(如 INT 和 VARCHAR),集合运算会发生什么?
6. 下一课
下一节我们将学习 约束与键 — 了解如何用 PRIMARY KEY、FOREIGN KEY 等约束保证数据完整性。