SQL 数据分析 · 指南 · 2026年10月30日 · 10 分钟阅读

UNION 和 UNION ALL 的区别(附 INTERSECT 和 EXCEPT),SQL 实例详解

两队乘客在登机口汇成一队,重复的登机牌被放到一边

UNION 和 UNION ALL 都是把两个查询的结果上下拼在一起。唯一的区别是:UNION 会删除合并结果中的重复行,UNION ALL 保留每一行。 去重需要额外的工作,所以 UNION ALL 也更快。给数据分析师的经验法则:默认用 UNION ALL,只有在你确实想要一份不重复的值列表时才用 UNION。同一家族还有两个运算符:INTERSECT 返回两个查询中都有的行,EXCEPT 返回第一个查询有、第二个查询没有的行。

所有例子都在 Nomto SQL 课程的航空公司数据库上运行,使用 PostgreSQL 语法,引擎是 DuckDB(Nomto SQL 练习场使用的引擎),每张结果表都是真实输出。

用一个小例子讲清四个运算符

aircrafts 表里有九种机型:

model range
Boeing 777-300 11100
Boeing 767-300 7900
Airbus A319-100 6700
Airbus A320-200 5700
Airbus A321-200 5600
Boeing 737-300 4200
Sukhoi Superjet-100 3000
Bombardier CRJ-200 2700
Cessna 208 Caravan 1200

取两个有重叠的集合:远程机型(航程至少 5,000 公里,共五种)和波音机型(共三种)。其中 777 和 767 两种机型同时属于两个集合。

UNION:全部,但不重复

SELECT model FROM aircrafts WHERE range >= 5000
UNION
SELECT model FROM aircrafts WHERE model LIKE 'Boeing%'
ORDER BY model;
model
Airbus A319-100
Airbus A320-200
Airbus A321-200
Boeing 737-300
Boeing 767-300
Boeing 777-300

五加三等于八,但两个集合里都有的两种波音机型只保留一次:结果是六行。

UNION ALL:全部,包括重复

SELECT model FROM aircrafts WHERE range >= 5000
UNION ALL
SELECT model FROM aircrafts WHERE model LIKE 'Boeing%'
ORDER BY model;
model
Airbus A319-100
Airbus A320-200
Airbus A321-200
Boeing 737-300
Boeing 767-300
Boeing 767-300
Boeing 777-300
Boeing 777-300

八行:UNION ALL 只是把第二个结果接在第一个后面。

INTERSECT:只保留两边都有的行

SELECT model FROM aircrafts WHERE range >= 5000
INTERSECT
SELECT model FROM aircrafts WHERE model LIKE 'Boeing%'
ORDER BY model;
model
Boeing 767-300
Boeing 777-300

EXCEPT:第一个有、第二个没有的行

SELECT model FROM aircrafts WHERE range >= 5000
EXCEPT
SELECT model FROM aircrafts WHERE model LIKE 'Boeing%'
ORDER BY model;
model
Airbus A319-100
Airbus A320-200
Airbus A321-200

和另外三个不同,EXCEPT 的结果取决于顺序。把两个查询对调,得到的是不属于远程机型的波音:只有 Boeing 737-300。Oracle 把这个运算符叫 MINUS;PostgreSQL、DuckDB、SQL Server 和 MySQL 8.0.31 及以上版本用 EXCEPT。

UNION 也会删除同一个查询内部的重复

一个常见误解是:UNION 只删除两半里同时出现的行。实际上它会删除最终结果里的所有重复,包括同一半内部的重复。看看从谢列梅捷沃(SVO)和圣彼得堡普尔科沃(LED)出发能飞到哪些机场:

SELECT arrival_airport FROM flights WHERE departure_airport = 'SVO'
INTERSECT
SELECT arrival_airport FROM flights WHERE departure_airport = 'LED'
ORDER BY 1;
arrival_airport
CSY
ESL
GOJ
KVX
NOJ
OVS
TJM
VKT

所以 UNION 相当于先 UNION ALL 再 SELECT DISTINCT。INTERSECT 和 EXCEPT 默认也返回不重复的行。

该用 UNION ALL 的场景

先把两列变成一列再计数

每个航班都有出发机场和到达机场。要统计每个机场的总起降次数(起飞加降落),先把两列上下叠成一列,再分组:

SELECT airport, COUNT(*) AS movements
FROM (
  SELECT departure_airport AS airport FROM flights
  UNION ALL
  SELECT arrival_airport FROM flights
) m
GROUP BY airport
ORDER BY movements DESC
LIMIT 5;
airport movements
DME 6434
SVO 5963
LED 3802
VKO 3436
OVB 2110

现在把 UNION ALL 换成 UNION 再运行一次:

airport movements
LED 1
VOZ 1
KXK 1
UCT 1
RTW 1

每个机场都只剩一次「起降」(所有值并列,所以你看到的机场顺序可能不同)。UNION 在 COUNT 看到数据之前,就把成千上万个相同的行压缩成了每个机场一行。查询不报错,数字乍一看也说得过去,这正是这个错误危险的地方。只要是先叠加行、再求计数或求和,你要的都是 UNION ALL。

用多个来源拼一张汇总表

UNION ALL 是把不同表的数字放进同一个结果的快捷方式:

SELECT 'airports' AS metric, COUNT(*) AS value FROM airports
UNION ALL
SELECT 'flights',  COUNT(*) FROM flights
UNION ALL
SELECT 'bookings', COUNT(*) FROM bookings
UNION ALL
SELECT 'tickets',  COUNT(*) FROM tickets;
metric value
airports 104
flights 33121
bookings 262788
tickets 366733

标签列让各行彼此区分:即使两张表的行数恰好相同,这两行也不一样,用 UNION 也不会被删掉。不过,如果最后没有 ORDER BY,数据库并不保证按你书写的顺序返回各部分。顺序重要的话,加一个排序列。

加一行合计

一份明细,下面紧跟总计,放在同一个结果里:

SELECT fare_conditions, COUNT(*) AS segments, ROUND(SUM(amount)) AS revenue
FROM ticket_flights
GROUP BY fare_conditions
UNION ALL
SELECT 'Total', COUNT(*), ROUND(SUM(amount))
FROM ticket_flights
ORDER BY segments;
fare_conditions segments revenue
Comfort 17291 5661169
Business 107642 55051796
Economy 920793 146956844
Total 1045726 207669809

更复杂的小计可以用 GROUP BY ROLLUP,不用重复写查询。

EXCEPT 和 INTERSECT 能帮上忙的场景

EXCEPT 是问「缺了什么」的简洁写法,数据核查里经常用到。从未出现在时刻表里的机型:

SELECT aircraft_code FROM aircrafts
EXCEPT
SELECT aircraft_code FROM flights;
aircraft_code
320

这是 Airbus A320-200:机队表里有它,但没有任何航班使用。同样的写法可以回答「从没买过票的会员」(这里 53,000 名会员中有 11,426 名)或者「上个月文件里有、这个月没有的客户」这类问题。

EXCEPT 比较的是整行,所以可以同时比较多列。要检查每条航线是否都有返程,把(出发,到达)组合和(到达,出发)组合做比较:SELECT departure_airport, arrival_airport FROM flights EXCEPT SELECT arrival_airport, departure_airport FROM flights。在这份时刻表里它返回零行,也就是说每条航线都是双向飞行的。

同样的问题也可以用 NOT EXISTS 或 LEFT JOIN ... IS NULL 来写(见 JOIN 详解里的反连接部分)。只需要键列时,EXCEPT 更短;需要缺失行的其他列时,NOT EXISTS 更灵活。有一个区别要记住:集合运算符把两个 NULL 视为相等,而 JOIN 条件 a.x = b.x 永远匹配不到 NULL。

合并查询的规则

所有集合运算符都要求两边的结构一致:

  1. 列数相同。 否则查询失败。DuckDB 的报错是:Set operations can only apply to expressions with the same number of result columns。
  2. 逐列类型兼容。 PostgreSQL 会拒绝 SELECT range FROM aircrafts UNION SELECT model FROM aircrafts,报错 “UNION types integer and character varying cannot be matched”。DuckDB 更宽松:它悄悄把数字转成文本,返回一个混合列(5600、Boeing 777-300、3000……)。不要依赖这一点;显式转换类型,查询才能在所有数据库里运行。
  3. 列按位置匹配,不按名字匹配。 如果第一个查询选 departure_airport, arrival_airport,第二个选 arrival_airport, departure_airport,它们就按这个顺序对齐。上面的返程检查正是利用了这一点;而无意中这样写,报表就会悄悄出错。
  4. 列名来自第一个查询。 SELECT 'x' AS a UNION ALL SELECT 'y' AS b 返回一列,名字是 a。
  5. 末尾的 ORDER BY 和 LIMIT 作用于整个结果。 要对其中一部分排序或限制行数,把它放进括号:
(SELECT model, range FROM aircrafts ORDER BY range DESC LIMIT 2)
UNION ALL
(SELECT model, range FROM aircrafts ORDER BY range LIMIT 2);
model range
Boeing 777-300 11100
Boeing 767-300 7900
Cessna 208 Caravan 1200
Bombardier CRJ-200 2700

航程最长的两种和最短的两种机型,放在同一个结果里。

混用运算符时,在 PostgreSQL 和标准 SQL 里 INTERSECT 的优先级高于 UNION 和 EXCEPT,所以 a UNION b INTERSECT c 的意思是 a UNION (b INTERSECT c)。与其记住这条规则,不如直接加括号。

INTERSECT ALL 和 EXCEPT ALL

PostgreSQL 和 DuckDB 还支持 INTERSECT 和 EXCEPT 的 ALL 版本,它们按次数处理重复,而不是直接去重。假设左边是 1, 1, 2:

它们适合逐行对账两张表,比如在重复行本身合理的情况下,比较源数据抽取和加载后的副本。MySQL 和 SQL Server 支持的变体较少,移植查询前先确认一下。

UNION 和 JOIN 的区别

两者都是合并两张表,但方向不同。UNION 增加行:结果和每个查询一样宽,长度是两者之和。JOIN 增加列:两张表的行按某个键匹配,并排放在一起。想在每个航班旁边显示机型,那是 JOIN;想把出发和到达放进同一个列表,那是 UNION。JOIN 的详细讲解见 SQL JOIN 用法详解。

常见问题

UNION 和 UNION ALL 有什么区别?

UNION 返回两个查询合并后的行,并删除重复。UNION ALL 返回所有行,包括重复的。UNION ALL 更快,因为它不需要比较行。

UNION ALL 比 UNION 快吗?

是的。为了去重,UNION 必须对整个结果排序或做哈希,在大表上既费时间又费内存。UNION ALL 只是简单追加。如果你知道两部分不可能重叠(比如标签不同,或者来自不同的日期范围),UNION ALL 用更少的工作得到同样的行。

UNION 会删除同一张表内部的重复吗?

会。UNION 删除整个合并结果里的重复,包括某一个查询内部重复的行。它的效果相当于 UNION ALL 之后再 DISTINCT。

UNION 可以和 ORDER BY 一起用吗?

可以,但只能在最后写一次;它对合并后的结果排序,可以引用第一个查询的列名或列的位置。要在合并前对某一部分排序,把那部分放进括号,并写上它自己的 ORDER BY 和 LIMIT。

EXCEPT 和 NOT IN 有什么区别?

两者都能找出一个集合里有、另一个集合里没有的行。EXCEPT 比较整行,并把 NULL 视为相等。NOT IN 只比较一列,而且只要子查询里有一个 NULL,就什么都不返回,这是一个经典陷阱。NOT EXISTS 是最稳妥的通用选择;只需要键列时,EXCEPT 最简短。

其他语言: English, Español, Português (Brasil), Tiếng Việt