📊 SQL 入门 Day 8:集合操作 — 用 SQL 做数学里的"并交差"

场景引入

你有两张表:一张存「2025 年 VIP 客户」,另一张存「2026 年 VIP 客户」。老板问:

  • ❓ 哪些客户两年都在?(交集)
  • ❓ 所有出现过 VIP 是谁?(并集)
  • ❓ 2025 年的 VIP 在 2026 年流失了哪些?(差集)

用 JOIN 能做一部分,但会很别扭。SQL 提供了更直接的工具------集合操作(Set Operations),把两个查询的结果集当成数学中的集合来运算。


一、UNION 与 UNION ALL --- 并集

语法

sql 复制代码
SELECT column_list FROM table_a
UNION [ALL]
SELECT column_list FROM table_b;

核心规则:两个 SELECT 的列数必须相同,对应列的数据类型必须兼容。

UNION vs UNION ALL

操作 去重 排序 性能
UNION 自动去重(类似 DISTINCT) 默认排序(有额外开销) 较慢
UNION ALL 保留全部行(含重复) 不排序 很快

示例

sql 复制代码
-- 2025年VIP
SELECT name, email FROM vip_2025
UNION
SELECT name, email FROM vip_2026;
-- → 所有出现过VIP的客户,重复只出现一次

SELECT name, email FROM vip_2025
UNION ALL
SELECT name, email FROM vip_2026;
-- → 两年都在的人会出现两次

执行结果示意(UNION):

sql 复制代码
   name    |       email
-----------+--------------------
 张三      | zhangsan@xxx.com
 李四      | lisi@xxx.com
 王五      | wangwu@xxx.com
 赵六      | zhaoliu@xxx.com
(4 行)

如果用了 UNION ALL,李四(两年都是 VIP)会显示两行。


二、INTERSECT --- 交集

语法

sql 复制代码
SELECT column_list FROM table_a
INTERSECT
SELECT column_list FROM table_b;

返回同时在两个查询结果中出现的行(自动去重)。

示例

sql 复制代码
-- 连续两年都是VIP的忠诚客户
SELECT name FROM vip_2025
INTERSECT
SELECT name FROM vip_2026;

执行结果示意

markdown 复制代码
  name
--------
 李四
(1 行)

💡 等价写法(用 JOIN):SELECT a.name FROM vip_2025 a JOIN vip_2026 b ON a.name = b.name;


三、EXCEPT --- 差集

语法

sql 复制代码
SELECT column_list FROM table_a
EXCEPT
SELECT column_list FROM table_b;

返回出现在第一个查询但不在第二个查询中的行(自动去重)。

🔔 注意 :PostgreSQL/Redshift 用 EXCEPT,MySQL 不支持 EXCEPT(需要用 NOT EXISTS / NOT IN 模拟),SQL Server 也是 EXCEPT,Oracle 用 MINUS

示例

sql 复制代码
-- 2025年有但2026年没有的VIP(流失客户)
SELECT name FROM vip_2025
EXCEPT
SELECT name FROM vip_2026;

执行结果示意

markdown 复制代码
  name
--------
 张三
 王五
(2 行)

等价写法(用 NOT EXISTS):

sql 复制代码
SELECT a.name FROM vip_2025 a
WHERE NOT EXISTS (
  SELECT 1 FROM vip_2026 b WHERE b.name = a.name
);

四、集合操作 vs JOIN --- 什么时候用哪个?

场景 推荐方式 原因
纵向拼接两个结果集 集合操作 JOIN 是横向拼接列,做不到纵向
找共同存在的记录 两者皆可 JOIN 更灵活(可带其他列)
找差集 集合操作或 NOT EXISTS EXCEPT 更简洁
需要其他列的数据 JOIN 集合操作只能操作 SELECT 列表

核心区别

  • JOIN → 横向拼接(增加列)
  • 集合操作 → 纵向拼接(增加行)

五、排序与去重

排序

集合操作的最终结果可以用 ORDER BY 排序,但只能写在最后一个查询之后

sql 复制代码
SELECT name, city FROM customers_north
UNION
SELECT name, city FROM customers_south
ORDER BY name;

❌ 错误写法:不能在每个子查询里单独 ORDER BY(除非加 LIMIT)。

去重

  • UNION / INTERSECT / EXCEPT 自带去重
  • 想保留重复 → UNION ALL
  • INTERSECT 和 EXCEPT 没有 ALL 版本(PostgreSQL 支持 INTERSECT ALL / EXCEPT ALL,但标准 SQL 不支持)

六、多集合操作与括号

可以链式组合多个集合操作:

sql 复制代码
SELECT id FROM table_a
EXCEPT
SELECT id FROM table_b
INTERSECT
SELECT id FROM table_c;

⚠️ 优先级:INTERSECT 优先级高于 UNION 和 EXCEPT。建议用括号明确:

sql 复制代码
(SELECT id FROM table_a EXCEPT SELECT id FROM table_b)
INTERSECT
(SELECT id FROM table_c);

七、实战:电商数据分析

sql 复制代码
-- 建表并插入模拟数据
CREATE TABLE orders_2024 (order_id INT, customer_id INT, amount DECIMAL);
CREATE TABLE orders_2025 (order_id INT, customer_id INT, amount DECIMAL);

INSERT INTO orders_2024 VALUES
(1,101,200),(2,102,150),(3,103,300),(4,104,250);
INSERT INTO orders_2025 VALUES
(5,101,180),(6,103,320),(7,105,400),(8,106,100);

-- ① 两年都有下单的客户(交集)
SELECT customer_id FROM orders_2024
INTERSECT
SELECT customer_id FROM orders_2025;
-- → 101, 103

-- ② 2024年所有下单客户 + 2025年所有下单客户(并集)
SELECT customer_id FROM orders_2024
UNION
SELECT customer_id FROM orders_2025
ORDER BY customer_id;
-- → 101, 102, 103, 104, 105, 106

-- ③ 2024年下单但2025年没下单的客户(差集 = 流失客户)
SELECT customer_id FROM orders_2024
EXCEPT
SELECT customer_id FROM orders_2025;
-- → 102, 104

-- ④ 用 UNION ALL 统计两年总订单量
SELECT '2024' AS year, COUNT(*) FROM orders_2024
UNION ALL
SELECT '2025' AS year, COUNT(*) FROM orders_2025;
-- 纵向拼接统计结果,非常清爽

⚠️ 注意事项

  1. 列名由第一个 SELECT 决定------后续 SELECT 的列别名会被忽略
  2. 列数必须相等 ------报错 number of columns must match
  3. ORDER BY 用第一个 SELECT 的列名/位置 ------ORDER BY 1ORDER BY name
  4. EXCEPT 不是所有数据库都支持 ------MySQL 需用 NOT IN / NOT EXISTS 替代
  5. NULL 处理------集合操作把 NULL 视为相同值(UNION 会去重 NULL)
  6. 性能 ------UNION ALLUNION 快得多(省去排序去重)

💡 记忆口诀

纵向拼接用集合,横向拼接用 JOIN; UNION 去重 ALL 保留,INTERSECT 找共有; EXCEPT 取差排第二,ORDER BY 放最后。

集合操作能让很多"跨表对比"的查询变得简洁优雅,是数据分析中的高频利器 🔥

相关推荐
码事漫谈5 小时前
告别数据孤岛与AI“水土不服”:金仓多模融合时序库如何让数据真正服务于业务
后端
IT_陈寒6 小时前
Redis的持久化配置把我坑惨了:你以为数据安全了?
前端·人工智能·后端
星栈6 小时前
Node 接口该写同步还是异步?
后端·node.js
红烧大青虫6 小时前
HarmonyOS应用开发实战:小事记 - UIAbility 的冷启动/热启动/后台启动三种场景与 launchParam 解析
后端·华为·harmonyos·鸿蒙系统
码事漫谈6 小时前
AI Token 缓存:命中省 10 倍,不命中白扔钱
后端
65岁退休Coder6 小时前
LangChain v1.3.4 笔记 - 04 Agent 中间件
后端
神奇小汤圆7 小时前
一个接口多个实现,Spring 怎么"适配多场景"?
后端
花开彼岸天~7 小时前
鸿蒙原生开发手记:徒步迹 - 自定义组件开发规范
后端·华为·harmonyos·鸿蒙系统
Irene19918 小时前
大数据开发的常见实践:把 SQL 直接写在 Shell 脚本里 ER图 数据血缘
sql·数据血缘·er