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)出发能飞到哪些机场:
- SVO 飞往 48 个不同的机场,LED 飞往 24 个,其中 8 个两地都有航班。
- 两份
arrival_airport列表做UNION,返回 64 行:48 + 24 − 8。 - 同样两个查询做
UNION ALL,返回 4,881 行,每个航班一行,因为时刻表里每有一次起飞,目的地就出现一次。
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。
合并查询的规则
所有集合运算符都要求两边的结构一致:
- 列数相同。 否则查询失败。DuckDB 的报错是:
Set operations can only apply to expressions with the same number of result columns。 - 逐列类型兼容。 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……)。不要依赖这一点;显式转换类型,查询才能在所有数据库里运行。 - 列按位置匹配,不按名字匹配。 如果第一个查询选
departure_airport, arrival_airport,第二个选arrival_airport, departure_airport,它们就按这个顺序对齐。上面的返程检查正是利用了这一点;而无意中这样写,报表就会悄悄出错。 - 列名来自第一个查询。
SELECT 'x' AS a UNION ALL SELECT 'y' AS b返回一列,名字是a。 - 末尾的
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:
- 对
1做EXCEPT ALL,返回1, 2:两个 1 中只抵消一个。 - 对
1, 1, 1做INTERSECT ALL,返回1, 1:个数取两边中较少的一边。
它们适合逐行对账两张表,比如在重复行本身合理的情况下,比较源数据抽取和加载后的副本。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