跳到主要内容

组合查询(UNION、INTERSECT、EXCEPT)

集合运算用于把多个查询结果纵向组合。它与 JOIN 的方向不同:JOIN 通常把相关列横向拼接,集合运算则让结构相同的行上下叠放或相互比较。

SELECT email FROM customers
UNION
SELECT email FROM newsletter_subscribers;

SQL 提供三种主要集合运算:

运算结果
UNION出现在左侧或右侧的行
INTERSECT同时出现在左侧和右侧的行
EXCEPT出现在左侧、但没有出现在右侧的行

MySQL 直到较新的版本才支持 INTERSECT 和 EXCEPT;Oracle 常用 MINUS 表示差集;具体支持应核对目标数据库及版本。

参与查询必须“并集兼容”​

集合运算两侧通常需要满足两个条件:

  1. 返回相同数量的列;
  2. 对应位置的列具有可转换或兼容的数据类型。
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 自动选择其他列。

检查清单​

  1. 每个分支的列数和对应类型是否兼容;
  2. 最左侧查询是否给出了期望的输出列名;
  3. 业务是否真的需要去重,还是应使用 ALL;
  4. 混合运算是否用括号明确了优先级;
  5. ORDER BY 和行数限制作用于分支还是整体;
  6. NULL 与重复行语义是否符合预期;
  7. 目标数据库是否支持 INTERSECT、EXCEPT 及其 ALL 变体。

集合运算的关键是先统一结果结构,再明确重复行规则。只要列位置、类型、运算范围和排序边界清楚,多来源数据就能被安全地组合。