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

SQL 子查询:WHERE、SELECT、FROM 里的子查询用法与实例

一块小的航班信息屏嵌在一块更大的机场出发信息屏里

子查询(subquery)就是写在另一个查询里、用括号括起来的 SELECT。外层查询把它的结果当作一个值、一个列表或一张表来用。所以子查询主要出现在三个位置,也对应三种形态:

此外,子查询要么是独立的(只执行一次,和外层的行无关),要么是相关的(引用了外层查询的列,逻辑上要对外层的每一行重新计算一次)。分清这几种情况之后,你遇到的绝大多数子查询都会变得很好读。

下面所有例子都在 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