SQL 数据分析 · 指南 · 2026年10月30日 · 14 分钟阅读
SQL 子查询:WHERE、SELECT、FROM 里的子查询用法与实例

子查询(subquery)就是写在另一个查询里、用括号括起来的 SELECT。外层查询把它的结果当作一个值、一个列表或一张表来用。所以子查询主要出现在三个位置,也对应三种形态:
- 在
WHERE里,作为单个值(total_amount > (SELECT AVG(...)))、作为列表(aircraft_code IN (SELECT ...)),或者配合EXISTS; - 在
SELECT里,为每一行算出一个值; - 在
FROM里,作为一整张表供外层继续查询(即派生表)。
此外,子查询要么是独立的(只执行一次,和外层的行无关),要么是相关的(引用了外层查询的列,逻辑上要对外层的每一行重新计算一次)。分清这几种情况之后,你遇到的绝大多数子查询都会变得很好读。
下面所有例子都在 Nomto SQL 课程使用的航空公司数据库上运行:2026 年夏季 104 个机场之间的 33,121 个航班、262,788 笔订单,以及机票、航段和会员计划数据,金额单位为美元。SQL 采用 PostgreSQL 语法,下面每个结果都是真实运行得到的。
WHERE 里的子查询:和单个值比较
最经典的第一个子查询:哪些订单的金额高于平均订单金额?你不能写 WHERE total_amount > AVG(total_amount),因为 WHERE 里不允许出现聚合函数。所以要在子查询里先算出平均值:
SELECT COUNT(*) AS bookings_above_avg
FROM bookings
WHERE total_amount > (SELECT AVG(total_amount) FROM bookings);
| bookings_above_avg |
|---|
| 87224 |
内层查询恰好返回一行一列:790.26 美元。这就是标量子查询,凡是允许出现单个值的地方都可以放它。数据库只执行一次,再拿每笔订单和这个数比较。262,788 笔订单里大约三分之一高于平均值,这在金额数据里很常见:少数大额订单会把均值拉高。
同样的写法可以找出取极值的那一行:
SELECT book_ref, book_date, total_amount
FROM bookings
WHERE total_amount = (SELECT MAX(total_amount) FROM bookings);
| book_ref | book_date | total_amount |
|---|---|---|
| 3B54BB | 2026-07-01 17:08:00 | 12045.00 |
如果有两笔订单并列最高,你会同时拿到两笔。这往往正是你想要的,也是它和 ORDER BY total_amount DESC LIMIT 1 的区别。
IN:和一个列表比较
子查询返回一列多行时,用 IN。哪些机型从谢列梅捷沃机场(SVO)起飞?
SELECT aircraft_code, model
FROM aircrafts
WHERE aircraft_code IN (
SELECT aircraft_code FROM flights WHERE departure_airport = 'SVO'
)
ORDER BY aircraft_code;
| aircraft_code | model |
|---|---|
| 319 | Airbus A319-100 |
| 321 | Airbus A321-200 |
| 733 | Boeing 737-300 |
| 763 | Boeing 767-300 |
| 773 | Boeing 777-300 |
| CN1 | Cessna 208 Caravan |
| CR2 | Bombardier CRJ-200 |
| SU9 | Sukhoi Superjet-100 |
内层查询返回 2,981 个机型代码,每个 SVO 出发航班一个,重复很多。IN 不在乎重复:每个机型要么在列表里,要么不在,所以每个机型只出现一次。
再对比一下你可能会写的连接(JOIN)版本:
SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT a.aircraft_code) AS models
FROM aircrafts a
JOIN flights f ON f.aircraft_code = a.aircraft_code
WHERE f.departure_airport = 'SVO';
| rows_returned | models |
|---|---|
| 2981 | 8 |
连接为每个匹配的航班返回一行,要用 DISTINCT 才能回到 8 个机型。这就是两者在实践中最主要的区别:WHERE 里的子查询只筛选行,不会让行数翻倍;连接则可能让行数翻倍。 如果你只想知道「有没有匹配」,子查询(或者下面要讲的 EXISTS)是更干净的工具。SQL JOIN 详解里详细讲了这种行数膨胀。
列表子查询里也可以有自己的 GROUP BY 和 HAVING。哪些城市有不止一个机场,各有几个?
SELECT city, COUNT(*) AS airports
FROM airports
WHERE city IN (
SELECT city FROM airports GROUP BY city HAVING COUNT(*) > 1
)
GROUP BY city
ORDER BY city;
| city | airports |
|---|---|
| Moscow | 3 |
| Ulyanovsk | 2 |
ANY 和 ALL
IN 是 = ANY (...) 的简写。ANY 和 ALL 也能和 >、< 等比较运算符一起用。哪些飞机的航程比机队里所有空客都远?
SELECT model, range
FROM aircrafts
WHERE range > ALL (SELECT range FROM aircrafts WHERE model LIKE 'Airbus%')
ORDER BY range DESC;
| model | range |
|---|---|
| Boeing 777-300 | 11100 |
| Boeing 767-300 | 7900 |
> ALL (...) 等价于 > (SELECT MAX(...)),多数人觉得 MAX 的写法更好读。ANY 和 ALL 更常出现在老代码和面试题里,新写的分析查询里反而少见。
EXISTS 和 NOT EXISTS:至少有一行匹配吗?
只要子查询返回至少一行,EXISTS (子查询) 就为真。子查询选了什么列并不重要,所以大家习惯写 SELECT 1。EXISTS 几乎总是相关子查询:内层查询引用外层的行。
有多少会员从没买过机票?
SELECT COUNT(*) AS members_without_tickets
FROM loyalty_members m
WHERE NOT EXISTS (
SELECT 1 FROM ticket_members tm WHERE tm.member_id = m.member_id
);
| members_without_tickets |
|---|
| 11426 |
逐行来读:对每个会员 m,找有没有关联到 m.member_id 的机票;如果一张都没有,就保留这个会员。53,000 名会员中,有 11,426 人加入了会员计划,但在这份数据里没有任何机票。
EXISTS 也能处理隔着两张表的条件。有多少订单至少包含一个商务舱航段?
SELECT COUNT(*) AS bookings_with_business
FROM bookings b
WHERE EXISTS (
SELECT 1
FROM tickets t
JOIN ticket_flights tf ON tf.ticket_no = t.ticket_no
WHERE t.book_ref = b.book_ref
AND tf.fare_conditions = 'Business'
);
| bookings_with_business |
|---|
| 83730 |
一笔订单有五个商务舱航段,也只算一次。用连接写同样的逻辑,就得用 COUNT(DISTINCT b.book_ref) 去掉重复。
NOT IN 的陷阱
NOT EXISTS 和 NOT IN 看起来可以互换,多数时候结果也一样。哪个机型在航班计划里一次都没出现?
SELECT aircraft_code
FROM aircrafts
WHERE aircraft_code NOT IN (SELECT aircraft_code FROM flights);
| aircraft_code |
|---|
| 320 |
空客 A320 在机队里,但这个夏天一个航班都没飞。这次能得到正确结果,是因为 flights.aircraft_code 没有 NULL。现在把同样的写法用在有 NULL 的列上。app_events 里,未登录访客的 member_id 是 NULL。哪些会员没有任何 App 事件?
SELECT COUNT(*) AS members_without_events
FROM loyalty_members
WHERE member_id NOT IN (SELECT member_id FROM app_events);
| members_without_events |
|---|
| 0 |
结果是 0,而且是错的。NOT EXISTS 版本给出真实答案:
SELECT COUNT(*) AS members_without_events
FROM loyalty_members m
WHERE NOT EXISTS (
SELECT 1 FROM app_events e WHERE e.member_id = m.member_id
);
| members_without_events |
|---|
| 19376 |
x NOT IN (列表) 的意思是「x 和列表里每个元素都不相等」。只要列表里有一个 NULL,x <> NULL 的结果就是未知,整个条件永远不会为真,所有行都被丢掉。这里的内层查询返回了 912,625 个 NULL。修正方法有两种:用 NOT EXISTS,或者写成 NOT IN (SELECT member_id FROM app_events WHERE member_id IS NOT NULL),后者同样返回 19,376。值得养成的习惯是:找「没有匹配的行」时,用 NOT EXISTS。背后的三值逻辑可以看 SQL NULL 值处理。
相关子查询:拿每一行和它所在的组比较
标量子查询也可以是相关的。有多少航段的价格高于它所在舱位的平均价?
SELECT tf.fare_conditions, COUNT(*) AS segments
FROM ticket_flights tf
WHERE tf.amount > (
SELECT AVG(t2.amount)
FROM ticket_flights t2
WHERE t2.fare_conditions = tf.fare_conditions
)
GROUP BY tf.fare_conditions
ORDER BY segments DESC;
| fare_conditions | segments |
|---|---|
| Economy | 291102 |
| Business | 29126 |
| Comfort | 6919 |
内层查询用到了外层行的 tf.fare_conditions,所以它返回的平均值取决于这一行:经济舱 159.60 美元,舒适舱 327.41 美元,商务舱 511.43 美元。同一张表的两个副本要起不同的别名(tf 和 t2),否则你没法说清楚指的是哪一个。
「逻辑上对每一行重新计算」并不代表数据库真的把内层查询跑了一百万次。PostgreSQL 和 DuckDB 都会把很多相关子查询改写成连接。不过,如果相关子查询很慢,同样的逻辑往往用窗口函数写更清楚:在派生表里写 amount > AVG(amount) OVER (PARTITION BY fare_conditions)。详见 SQL 窗口函数详解。
SELECT 里的子查询:每行一个值
SELECT 列表里的标量子查询会给每个输出行加上一个计算出来的值。每种机型有多少座位、执飞多少航班?
SELECT a.model,
(SELECT COUNT(*) FROM seats s WHERE s.aircraft_code = a.aircraft_code) AS seats,
(SELECT COUNT(*) FROM flights f WHERE f.aircraft_code = a.aircraft_code) AS flights
FROM aircrafts a
ORDER BY seats DESC;
| model | seats | flights |
|---|---|---|
| Boeing 777-300 | 402 | 610 |
| Boeing 767-300 | 222 | 1221 |
| Airbus A321-200 | 170 | 1952 |
| Airbus A320-200 | 140 | 0 |
| Boeing 737-300 | 130 | 1274 |
| Airbus A319-100 | 116 | 1239 |
| Sukhoi Superjet-100 | 97 | 8504 |
| Bombardier CRJ-200 | 50 | 9048 |
| Cessna 208 Caravan | 12 | 9273 |
这是把两张不同表的计数并排放在一起的简洁写法。如果把 aircrafts 同时连接 seats 和 flights 再分组,座位数会和航班数相乘,结果毫无意义。还要注意 A320 得到的是 0,而不是缺一行:对零行做 COUNT(*) 结果是 0。
SELECT 里的非相关标量子查询很适合算占总数的比例:
SELECT model,
(SELECT COUNT(*) FROM seats s WHERE s.aircraft_code = a.aircraft_code) AS seats,
ROUND(100.0 * (SELECT COUNT(*) FROM seats s WHERE s.aircraft_code = a.aircraft_code)
/ (SELECT COUNT(*) FROM seats), 1) AS pct_of_all_seats
FROM aircrafts a
ORDER BY seats DESC
LIMIT 4;
| model | seats | pct_of_all_seats |
|---|---|---|
| Boeing 777-300 | 402 | 30 |
| Boeing 767-300 | 222 | 16.6 |
| Airbus A321-200 | 170 | 12.7 |
| Airbus A320-200 | 140 | 10.5 |
能用,但座位数的子查询写了两遍。看到同一个子查询重复出现时,就该把它挪到 CTE 或派生表里了。
FROM 里的子查询:派生表
FROM 里的子查询返回一整张表,外层查询把它当成普通表来用。这就是两次聚合的写法:先按订单聚合,再跨订单聚合。一笔订单通常包含几张机票?
SELECT tickets_in_booking, COUNT(*) AS bookings
FROM (
SELECT book_ref, COUNT(*) AS tickets_in_booking
FROM tickets
GROUP BY book_ref
) AS per_booking
GROUP BY tickets_in_booking
ORDER BY tickets_in_booking;
| tickets_in_booking | bookings |
|---|---|
| 1 | 173390 |
| 2 | 75793 |
| 3 | 12686 |
| 4 | 896 |
| 5 | 23 |
内层查询为每笔订单产生一行,带上机票数;外层查询再对这些行分组。单个 GROUP BY 做不到这一点,因为不允许写 COUNT(COUNT(*))。用同一个派生表还能算出平均值:SELECT ROUND(AVG(tickets_in_booking), 3) FROM (...) AS per_booking,结果是每笔订单 1.396 张机票。
派生表也可以和连接组合。这里先按机场统计出发航班数,然后才连接 airports 拿城市名:
SELECT a.city, d.departures
FROM (
SELECT departure_airport, COUNT(*) AS departures
FROM flights
WHERE status = 'Arrived'
GROUP BY departure_airport
) AS d
JOIN airports a ON a.airport_code = d.departure_airport
ORDER BY d.departures DESC
LIMIT 5;
| city | departures |
|---|---|
| Moscow | 1618 |
| Moscow | 1503 |
| St. Petersburg | 956 |
| Moscow | 869 |
| Novosibirsk | 536 |
莫斯科出现了三次,因为它有三个机场(DME、SVO、VKO),而分组是按机场做的。如果想要每个城市一行,就在连接之后按 a.city 分组:莫斯科合计 3,990 个出发航班。两种都不算错;子查询让你一眼看出是在哪个层级聚合的。
LATERAL:在 FROM 里能看到外层行的子查询
普通的派生表不能引用同一个 FROM 里的其他表。LATERAL 取消了这个限制,用来写「每组前 N 名」很方便。SVO 和 LED 各自最常飞往的两个目的地:
SELECT ap.airport_code, ap.city, top.arrival_airport, top.flights
FROM airports ap
CROSS JOIN LATERAL (
SELECT f.arrival_airport, COUNT(*) AS flights
FROM flights f
WHERE f.departure_airport = ap.airport_code
GROUP BY f.arrival_airport
ORDER BY flights DESC, f.arrival_airport
LIMIT 2
) AS top
WHERE ap.airport_code IN ('SVO', 'LED')
ORDER BY ap.airport_code, top.flights DESC;
| airport_code | city | arrival_airport | flights |
|---|---|---|---|
| LED | St. Petersburg | SVO | 305 |
| LED | St. Petersburg | DME | 244 |
| SVO | Moscow | LED | 305 |
| SVO | Moscow | BZK | 183 |
LATERAL 在 PostgreSQL、DuckDB 和 MySQL 8 里都能用;SQL Server 的写法是 CROSS APPLY。对多数分析师来说,用 ROW_NUMBER() 求前 N 名更常见,但认得 LATERAL 还是有用的。
子查询、JOIN 还是 CTE?
| 你需要 | 通常最合适 |
|---|---|
| 按「有匹配」/「没有匹配」筛选 | EXISTS / NOT EXISTS |
| 按一个短列表或单个计算值筛选 | IN / WHERE 里的标量子查询 |
| 输出里要有另一张表的列 | JOIN |
| 先聚合,再聚合一次 | 派生表或 CTE |
| 同一个中间结果用两次,或嵌套超过两层 | CTE |
| 每行取另一张表的一个值,又不想行数膨胀 | SELECT 里的标量子查询,或先聚合再连接 |
性能很少是决定因素:现代优化器会在内部把 IN、EXISTS 和很多相关子查询改写成连接。选最能直接表达问题的写法,真的慢了再去看执行计划。
常见错误
1. 标量子查询返回了不止一行
SELECT (SELECT aircraft_code FROM flights WHERE departure_airport = 'SVO') AS x;
PostgreSQL 会报错「more than one row returned by a subquery used as an expression」,DuckDB 的报错信息类似。当作单个值使用的子查询最多只能返回一行:对它做聚合(MAX、COUNT),把它筛到只剩一行,或者如果你本来就想要一个列表,就改用 IN。如果它一行都不返回,值就是 NULL,可能会让 WHERE 悄悄筛掉所有行。
2. 对含 NULL 的列用 NOT IN
上面讲过:列表里只要有一个 NULL,NOT IN 就什么都不返回。改用 NOT EXISTS。
3. 没加表名前缀的列悄悄指向了外层查询
SELECT COUNT(*) AS models
FROM aircrafts
WHERE aircraft_code IN (SELECT aircraft_code FROM airports);
| models |
|---|
| 9 |
airports 根本没有 aircraft_code 这一列,可是查询照样运行,还返回了全部 9 个机型。SQL 先在内层表里找 aircraft_code,没找到,就去外层的 aircrafts 行里取,于是条件变成了「aircraft_code 等于它自己」。在子查询里一定要给列加上表别名;这样拼写错误会直接报错,而不是给出错误答案。
4. 派生表没有别名
SELECT COUNT(*) FROM (SELECT 1 FROM aircrafts) 在 DuckDB 和 PostgreSQL 16 及以上版本里能运行。PostgreSQL 15 及更早版本,以及一些其他数据库,会报错「subquery in FROM must have an alias」。写上 AS t 不费事,而且到处都能用。
5. 子查询里的 ORDER BY
派生表里的 ORDER BY 不能保证最终结果的顺序。排序要写在最外层查询。在子查询里,ORDER BY 只有和 LIMIT 一起用才有意义,就像 LATERAL 那个例子。
常见问题
SQL 里的子查询是什么?
嵌套在另一条语句里、用括号括起来的 SELECT 语句。根据它所在的位置,外层查询把它的结果当作单个值、一个值列表或一张表来用。
相关子查询和非相关子查询有什么区别?
非相关子查询不引用外层查询,可以单独运行,只计算一次。相关子查询引用了外层查询的列(WHERE t2.fare_conditions = tf.fare_conditions),所以它的结果取决于当前的外层行。
子查询比 JOIN 慢吗?
本质上不会。PostgreSQL、DuckDB 和其他现代引擎会把大多数 IN 和 EXISTS 子查询改写成连接。写最能表达问题的版本,只有真的慢了才去看执行计划。
什么时候该用 EXISTS 而不是 IN?
条件依赖外层行或涉及多列时用 EXISTS。找「没有匹配的行」时,永远优先用 NOT EXISTS 而不是 NOT IN,因为列表里一旦有 NULL,NOT IN 就会失效。
子查询能返回多列吗?
在 FROM 里可以:派生表可以有任意多列。在 WHERE 里配合 IN 时,子查询通常只返回一列,不过 PostgreSQL 也支持 (a, b) IN (SELECT x, y ...) 这样的行比较。在 SELECT 里,子查询必须返回单个值。
其他语言: English, Español, Português (Brasil), Tiếng Việt