组合查询(UNION、INTERSECT、EXCEPT)
集合运算用于把多个查询结果纵向组合。它与 JOIN 的方向不同:JOIN 通常把相关列横向拼接,集合运算则让结构相同的行上下叠放或相互比较。
SELECT email FROM customers
UNION
SELECT email FROM newsletter_subscribers;
SQL 提供三种主要集合运算:
| 运算 | 结果 |
|---|---|
UNION | 出现在左侧或右侧的行 |
INTERSECT | 同时出现在左侧和右侧的行 |
EXCEPT | 出现在左侧、但没有出现在右侧的行 |
MySQL 直到较新的版本才支持 INTERSECT 和 EXCEPT;Oracle 常用 MINUS 表示差集;具体支持应核对目标数据库及版本。
参与查询必须“并集兼容”
集合运算两侧通常需要满足两个条件:
- 返回相同数量的列;
- 对应位置的列具有可转换或兼容的数据类型。
SELECT customer_id, name
FROM current_customers
UNION ALL
SELECT customer_id, name
FROM archived_customers;
对应关系按位置确定,不按列名确定。下面两个查询的第二列虽然名字不同,只要类型兼容,仍可以组合:
SELECT user_id, display_name
FROM users
UNION ALL
SELECT member_id, member_name
FROM members;
最终结果的列名通常来自最左侧查询,因此建议在第一条查询中设置清楚的别名:
SELECT user_id AS id, display_name AS name
FROM users
UNION ALL
SELECT member_id, member_name
FROM members;
不同数据库的公共类型推断规则不同。数字与文本、日期与时间戳、不同字符排序规则混合时,应使用 CAST 明确统一类型,而不是依赖隐式转换。
UNION 与 UNION ALL
UNION 合并两侧结果并删除重复行:
SELECT city FROM customers
UNION
SELECT city FROM suppliers;
UNION ALL 保留所有行,包括每侧内部及两侧之间的重复:
SELECT city FROM customers
UNION ALL
SELECT city FROM suppliers;
如果客户表中有两个“上海”,供应商表中又有一个“上海”,UNION ALL 会保留三行;UNION 只保留一行。
当业务不要求去重时,应优先使用 UNION ALL。UNION 通常需要额外的排序或哈希处理,在大结果集上可能明显增加内存、CPU 和临时磁盘开销。
一个常见用法是合并结构相同的当前表与历史表:
SELECT order_id, customer_id, total_amount, 'current' AS source
FROM orders
UNION ALL
SELECT order_id, customer_id, total_amount, 'archive' AS source
FROM archived_orders;
额外的 source 列可以保留数据来源。若不需要来源且两个表可能包含同一业务记录,应明确决定是保留重复、整体去重,还是按业务键选择一条。
INTERSECT:取得交集
INTERSECT 只返回两侧都存在的行:
SELECT customer_id
FROM orders
WHERE created_at >= DATE '2026-01-01'
INTERSECT
SELECT customer_id
FROM support_tickets
WHERE created_at >= DATE '2026-01-01';
结果表示今年既下过订单、又提交过工单的客户。普通 INTERSECT 会去重。
部分数据库支持 INTERSECT ALL。它按重复次数计算交集:某一行在左侧出现 m 次、右侧出现 n 次,结果保留 min(m, n) 次。并非所有产品都支持 ALL 变体。
交集也常能用内连接或两个 EXISTS 改写,但要注意重复行语义。集合运算比较的是整行,连接则由显式连接条件决定。
EXCEPT:取得差集
EXCEPT 返回左侧存在而右侧不存在的行:
SELECT customer_id
FROM customers
EXCEPT
SELECT customer_id
FROM orders;
结果是从未产生订单的客户编号。EXCEPT 有方向,交换两侧会得到不同结果:
A EXCEPT B != B EXCEPT A
普通 EXCEPT 会去重。支持 EXCEPT ALL 的数据库按重复次数相减:左侧出现 m 次、右侧出现 n 次,结果保留 max(m - n, 0) 次。
如果右侧可能含有 NULL,EXCEPT 往往比 NOT IN (subquery) 更容易表达整行差集,因为 NOT IN 会受到三值逻辑影响。不过,按关联条件排除行时,NOT EXISTS 通常更灵活:
SELECT c.customer_id
FROM customers AS c
WHERE NOT EXISTS (
SELECT 1
FROM orders AS o
WHERE o.customer_id = c.customer_id
);
重复行与 NULL
集合运算的去重不是普通的逐列 = 比较。用于 UNION、INTERSECT 和 EXCEPT 的集合语义通常会把对应位置都为 NULL 的行视为重复行:
SELECT NULL AS value
UNION
SELECT NULL AS value;
结果通常只有一行。使用 ALL 时则保留或按次数计算重复行。
对多列结果,只有整行各对应值都被集合语义视为相同时,才属于同一个重复项。
运算优先级与结合方向
混合多种集合运算时,不要只靠换行判断范围。标准 SQL 中,INTERSECT 通常比 UNION 和 EXCEPT 优先级高,而同级运算一般从左向右结合:
query_a
UNION
query_b
INTERSECT
query_c
通常相当于:
query_a
UNION
(query_b INTERSECT query_c)
为了避免数据库差异和阅读误解,应使用括号表达真正意图:
(
SELECT customer_id FROM online_orders
UNION
SELECT customer_id FROM store_orders
)
EXCEPT
SELECT customer_id FROM blocked_customers;
ORDER BY、LIMIT 应放在哪里
对整个组合结果排序时,把 ORDER BY 写在最后:
SELECT customer_id, created_at
FROM current_orders
UNION ALL
SELECT customer_id, created_at
FROM archived_orders
ORDER BY created_at DESC, customer_id;
最后的 ORDER BY 作用于整个集合运算结果。它只能可靠地引用最终输出列的名称、位置或目标数据库允许的表达式。
如果只想限制某个分支,必须用括号明确边界:
(
SELECT order_id, created_at
FROM current_orders
ORDER BY created_at DESC, order_id DESC
FETCH FIRST 10 ROWS ONLY
)
UNION ALL
SELECT order_id, created_at
FROM pinned_orders;
没有括号时,末尾的 ORDER BY 和行数限制通常属于整个组合查询。分支内部只写 ORDER BY 而不限制行数通常没有意义,因为集合结果最终仍然无序。
与 JOIN 的区别
| 需求 | 更适合的工具 |
|---|---|
| 把结构相同的当前数据和历史数据上下合并 | UNION ALL |
| 合并来源并删除完全重复的行 | UNION |
| 找出两组完整结果共同拥有的行 | INTERSECT |
| 找出只存在于第一组的完整行 | EXCEPT |
| 按键关联两张表并取得双方不同列 | JOIN |
| 判断关联记录是否存在 | EXISTS / NOT EXISTS 通常更直接 |
不要为了“把两张表放在一起”就默认选择 UNION。先判断需要增加行,还是增加列。
实用示例:统一事件流
假设支付和退款分别存储在两张表中,可以先统一为相同结构,再排序:
SELECT
payment_id AS event_id,
order_id,
paid_at AS event_time,
amount,
'payment' AS event_type
FROM payments
UNION ALL
SELECT
refund_id AS event_id,
order_id,
refunded_at AS event_time,
-amount AS amount,
'refund' AS event_type
FROM refunds
ORDER BY event_time DESC, event_type, event_id DESC;
这个查询不仅统一了列名,也把退款金额转换为负数,并保留事件类型。由于支付与退款本来就是不同事件,不需要承担去重成本,所以使用 UNION ALL。
常见错误
两侧列数或位置不匹配
集合运算按位置对应。应显式列出字段,避免 SELECT * 因表结构变化造成列数或顺序不一致。
无意中使用 UNION 去重
去重可能改变业务数量,也会增加执行成本。若只是合并数据源,先考虑 UNION ALL。
把分支排序误认为最终排序
结果顺序只有最外层 ORDER BY 才能保证。分支顺序和读取顺序都不是最终结果契约。
忽略运算优先级
混合 UNION、INTERSECT、EXCEPT 时主动加括号,并用小型测试数据验证边界情况。
只对部分列“去重”
集合运算比较完整输出行。若需求是按业务键保留最新记录,应使用窗口函数或分组策略,而不是指望 UNION 自动选择其他列。
检查清单
- 每个分支的列数和对应类型是否兼容;
- 最左侧查询是否给出了期望的输出列名;
- 业务是否真的需要去重,还是应使用
ALL; - 混合运算是否用括号明确了优先级;
ORDER BY和行数限制作用于分支还是整体;NULL与重复行语义是否符合预期;- 目标数据库是否支持
INTERSECT、EXCEPT及其ALL变体。
集合运算的关键是先统一结果结构,再明确重复行规则。只要列位置、类型、运算范围和排序边界清楚,多来源数据就能被安全地组合。