📊 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 1 或 ORDER BY name
  4. EXCEPT 不是所有数据库都支持 ------MySQL 需用 NOT IN / NOT EXISTS 替代
  5. NULL 处理------集合操作把 NULL 视为相同值(UNION 会去重 NULL)
  6. 性能 ------UNION ALL 比 UNION 快得多(省去排序去重)

💡 记忆口诀

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

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

相关推荐
乌暮1 小时前
Spring AOP 与 Spring 事务:用大白话彻底理解原理与实践
java·数据库·spring boot·后端·sql·学习·spring
我不会起名字3221 小时前
未接入最终读上限的参数:`websocketMaxMsgLen` 与 12.5 倍差距
后端·websocket·go·即时通讯·im·故障排查
一条Go2 小时前
接收方合并去重:同一个人为什么只该收到一次通知
后端·架构·github
张一凡933 小时前
给 Coding Agent 加一道"架构闸门":Jin 是如何做到"先审架构、再写代码、不漂移"的
前端·后端·agent
Jvav练习生3 小时前
AI Agent 总在关键时刻“失忆”翻车?一文讲透上下文工程(附可运行 Python 代码)
后端
leo在掘金3 小时前
只挂了11小时,5000台开发机被塞进SSH公钥
后端·架构
xjz18423 小时前
Rust 运算符优先级:括号不只用来改变顺序
后端
写码跑山的老艾3 小时前
为什么有了 MySQL,还要 Redis 和 Elasticsearch?从一件商品看数据系统的权衡
后端
对象存储与RustFS3 小时前
Jenkins 的构建产物别堆在控制器磁盘:用 RustFS 当它的 S3 工件仓库
后端·docker·云原生