子查询
本教程共 46 篇 · 第 24 篇 · 更新于 2026-07-30 · 约 11 分钟阅读
24. 子查询
本节目标:理解子查询的概念和分类,掌握标量子查询、行子查询、表子查询的用法,学会用 IN/ANY/ALL 配合子查询,区分相关子查询与非相关子查询,理解子查询的性能特点。
24.1 什么是子查询
子查询(Subquery)就是查询里面嵌套的查询。一个 SELECT 语句的结果作为另一个 SQL 语句的一部分。
-- 查年龄大于平均年龄的用户
SELECT * FROM users
WHERE age > (
SELECT AVG(age) FROM users
);
括号里的 SELECT AVG(age) FROM users 就是子查询。它先算出平均年龄,外层查询再拿这个平均值做比较。
子查询可以出现在 SQL 的很多位置:
| 位置 | 示例 |
|---|---|
| WHERE 条件中 | WHERE age > (SELECT AVG(age) FROM users) |
| SELECT 列中 | SELECT username, (SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS 订单数 FROM users |
| FROM 子句中 | SELECT * FROM (SELECT * FROM users WHERE age > 20) AS t |
| HAVING 条件中 | HAVING SUM(amount) > (SELECT AVG(amount) FROM orders) |
24.2 按返回结果分类
子查询按返回结果的行列数,分三种:
标量子查询(Scalar Subquery)
返回一行一列,就是一个值。可以用在需要单个值的地方(比较运算符右边、SELECT 列中等)。
-- 查比平均年龄大的用户
SELECT * FROM users
WHERE age > (SELECT AVG(age) FROM users);
-- 在 SELECT 中用
SELECT username,
age,
(SELECT MAX(age) FROM users) AS 最大年龄,
age - (SELECT AVG(age) FROM users) AS 与平均差
FROM users;
Note标量子查询必须只返回一个值。如果返回多行,MySQL 会报错:
Subquery returns more than 1 row。
行子查询(Row Subquery)
返回一行多列,可以跟一行多列做比较:
-- 查和 id=1 的用户同名同龄的用户
SELECT * FROM users
WHERE (username, age) = (
SELECT username, age FROM users WHERE id = 1
);
行子查询用括号把多列组合起来比较,两边列数和顺序要一致。
表子查询(Table Subquery)
返回多行多列,相当于一张临时表。通常用在 FROM 子句或 IN/ANY/ALL 条件中。
-- 用在 FROM 中(这叫派生表,下一章详讲)
SELECT * FROM (
SELECT user_id, SUM(amount) AS 总消费
FROM orders
GROUP BY user_id
) AS t
WHERE t.总消费 > 1000;
-- 用在 IN 中
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 500);
24.3 IN 子查询
IN 配合子查询是最常见的用法之一。子查询返回一列多行,外层查”在这一列值里”的行。
-- 查有订单的用户
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders);
-- 查没有订单的用户
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM orders);
IN 子查询的执行逻辑:先执行子查询拿到一个值列表,再拿外层的每行去匹配这个列表。
Warning
NOT IN子查询里如果有 NULL,会导致查询返回空结果。这在第 20 章讲过。用NOT IN时务必在子查询中排除 NULL:SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders WHERE user_id IS NOT NULL);或者直接用
NOT EXISTS(下一章讲),更安全。
24.4 ANY 和 ALL
ANY 和 ALL 配合比较运算符使用,处理子查询返回的多个值。
ANY:任意一个
ANY 表示”只要满足子查询结果中的任意一个值”:
-- 查年龄大于任意一个 VIP 用户年龄的用户
-- 等价于:大于最小 VIP 用户年龄
SELECT * FROM users
WHERE age > ANY (SELECT age FROM users WHERE id <= 3);
> ANY 等价于”大于子查询结果中的最小值”。
ALL:所有
ALL 表示”必须满足子查询结果中的所有值”:
-- 查年龄大于所有 VIP 用户年龄的用户
-- 等价于:大于最大 VIP 用户年龄
SELECT * FROM users
WHERE age > ALL (SELECT age FROM users WHERE id <= 3);
> ALL 等价于”大于子查询结果中的最大值”。
| 写法 | 含义 | 等价于 |
|---|---|---|
> ANY (子查询) | 大于任意一个 | 大于最小值 |
> ALL (子查询) | 大于所有 | 大于最大值 |
< ANY (子查询) | 小于任意一个 | 小于最大值 |
< ALL (子查询) | 小于所有 | 小于最小值 |
= ANY (子查询) | 等于任意一个 | 等同于 IN |
<> ALL (子查询) | 不等于所有 | 等同于 NOT IN |
TipANY 和 ALL 在实际开发中用得不多,因为大多数场景可以用 MIN/MAX 聚合函数替代,可读性更好:
-- > ANY 等价于 SELECT * FROM users WHERE age > (SELECT MIN(age) FROM users WHERE id <= 3); -- > ALL 等价于 SELECT * FROM users WHERE age > (SELECT MAX(age) FROM users WHERE id <= 3);
24.5 相关子查询与非相关子查询
这是子查询最重要的分类,影响执行方式和性能。
非相关子查询
子查询不引用外层查询的列,可以独立执行:
-- 子查询跟外层无关,只执行一次
SELECT * FROM users
WHERE age > (SELECT AVG(age) FROM users);
(SELECT AVG(age) FROM users) 不依赖外层的任何列,执行一次拿到平均值,外层拿这个值做比较。
相关子查询
子查询引用了外层查询的列,外层每处理一行,子查询就要执行一次:
-- 子查询引用了外层的 users.id
SELECT username,
(SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS 订单数
FROM users;
子查询里的 users.id 来自外层查询。外层每取一行用户,子查询就执行一次,查这个用户有多少订单。
-- 另一个相关子查询:查消费超过 1000 的用户
SELECT * FROM users u
WHERE 1000 < (
SELECT SUM(amount) FROM orders WHERE user_id = u.id
);
性能对比
| 类型 | 执行次数 | 性能 |
|---|---|---|
| 非相关子查询 | 1 次 | 好 |
| 相关子查询 | 外层行数次 | 差(数据量大时) |
相关子查询在大表上性能很差,因为子查询被反复执行。
Warning相关子查询如果外层有 10 万行,子查询就执行 10 万次。这种情况下要考虑改写为 JOIN:
-- 相关子查询(慢) SELECT * FROM users u WHERE 1000 < (SELECT SUM(amount) FROM orders WHERE user_id = u.id); -- 改写为 JOIN + GROUP BY(快) SELECT u.* FROM users u INNER JOIN orders o ON o.user_id = u.id GROUP BY u.id HAVING SUM(o.amount) > 1000;
24.6 子查询在 SELECT 中
子查询可以作为 SELECT 的一个列,给每行附加一个计算值:
SELECT
u.username,
u.age,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS 订单数,
(SELECT SUM(amount) FROM orders o WHERE o.user_id = u.id) AS 总消费
FROM users u;
这种写法直观,但每行都要执行两个子查询。如果 users 表有 1000 行,就执行 2000 次子查询。
Tip| 这种”N+1 查询”问题在应用层也很常见(循环里查数据库)。用 JOIN + GROUP BY 改写能大幅提升性能:
SELECT u.username, u.age, COUNT(o.id) AS 订单数, IFNULL(SUM(o.amount), 0) AS 总消费 FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id, u.username, u.age;
24.7 子查询在 UPDATE 和 DELETE 中
子查询不只是 SELECT 的专利,UPDATE 和 DELETE 里也能用。
-- 给消费总额超过 1000 的用户改标记
UPDATE users
SET email = CONCAT(username, '@vip.com')
WHERE id IN (
SELECT user_id FROM orders
GROUP BY user_id
HAVING SUM(amount) > 1000
);
-- 删除没有订单的用户的过期订单记录(逻辑示例)
DELETE FROM orders
WHERE user_id NOT IN (SELECT id FROM users);
NoteMySQL 不允许在 UPDATE 或 DELETE 的子查询中引用正在修改的那张表。比如不能
DELETE FROM users WHERE id IN (SELECT user_id FROM users WHERE age < 18),会报错。解决方法是把子查询包一层派生表:DELETE FROM users WHERE id IN (SELECT t.id FROM (SELECT id FROM users WHERE age < 18) AS t);
24.8 子查询 vs JOIN:怎么选
很多子查询都能改写成 JOIN,反之亦然。选择原则:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 需要从另一张表取一个值比较 | 标量子查询 | 语义清晰 |
| 需要判断是否存在关联数据 | EXISTS(下一章) | 性能好 |
| 需要把两张表的数据拼在一起显示 | JOIN | 天然适合 |
| 需要按另一张表的数据过滤 | IN 子查询或 JOIN | 都行,看可读性 |
| 相关子查询涉及聚合 | JOIN + GROUP BY | 性能更好 |
Tip别为了炫技把简单查询写成多层嵌套子查询。能 JOIN 就 JOIN,子查询一层最好,超过两层就该考虑拆成多步或用 CTE(后面章节讲)。
24.9 小结
子查询是 SQL 的”套娃”能力。标量子查询返回一个值,行子查询返回一行,表子查询返回一张临时表。IN 配合子查询做集合匹配,ANY/ALL 做范围比较。相关子查询依赖外层每行执行一次,性能差,能改 JOIN 就改。子查询虽灵活,但别过度嵌套,简洁的 SQL 才是好 SQL。下一章学派生表和 EXISTS。