首页 / MySQL 入门教程 / 子查询

MySQL 入门教程

子查询

本教程共 46 篇 · 第 24 篇 · 更新于 2026-07-30 · 约 11 分钟阅读

MySQLMySQL 入门教程子查询SubqueryINANYALL相关子查询

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

ANYALL 配合比较运算符使用,处理子查询返回的多个值。

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
Tip

ANY 和 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);
Note

MySQL 不允许在 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。