跳到主要内容

04 子查询

📖 本节导读

  • 理解什么是子查询:查询里再套一个查询,先算里面、再算外面。
  • 掌握两种最常用的子查询:标量子查询(返回一个值)和列子查询(配合 IN 使用)。
  • 学会把子查询放进 FROM 当"派生表"用(必须起别名),并认识 EXISTS。
  • 知道子查询和连接查询各自适合什么场景,学会二者之间怎么选。

一、什么是子查询

需求:查出所有高于全校平均分的成绩

按人脑思路要分两步:

  1. 先算出全校平均分:78.90909;
  2. 再查 score > 78.90909 的成绩。

难道每次都要人工先查一遍平均分,再抄进第二条 SQL 吗?不用——SQL 允许把第一步整条查询塞进第二步的括号里:

SELECT ... WHERE score > (SELECT AVG(score) FROM score);

括号里的查询叫子查询(也叫内层查询),外面的叫主查询(外层查询)。执行规则和数学里的括号一样:先算里面,再算外面——MySQL 先执行子查询得到 78.90909,再拿这个值去跑外层查询。

子查询按"返回结果的形状"分类,用法各不相同:

类型返回形状常配合的操作符
标量子查询一行一列(一个值)= > < >= <= <>
列子查询多行一列IN、ANY、ALL
行子查询一行多列= (a, b)
表子查询多行多列放在 FROM 后当表

二、标量子查询:返回一个值

"标量"听着唬人,其实就是"单个值"。上面的需求完整写出来(顺便连表显示姓名):

SELECT s.name AS 学生, sc.course_id, sc.score AS 成绩
FROM score AS sc
INNER JOIN student AS s ON sc.student_id = s.id
WHERE sc.score > (SELECT AVG(score) FROM score);
+-----------+-----------+--------+
| 学生 | course_id | 成绩 |
+-----------+-----------+--------+
| 张三 | 1 | 90.0 |
| 张三 | 2 | 85.0 |
| 张三 | 3 | 88.0 |
| 李四 | 2 | 92.0 |
| 王小红 | 1 | 95.0 |
| 赵敏 | 3 | 81.0 |
+-----------+-----------+--------+
6 rows in set (0.00 sec)

全校平均分是 868 ÷ 11 = 78.90909(AVG 忽略陈刚的 NULL),高于它的成绩正好是这 6 条。76.0、73.0、70.0、60.0、58.0 都不够,NULL 与任何值比较结果都不成立,也不会出现。

再来一个:查出年龄最小(生日最大)的学生:

SELECT name, birthday
FROM student
WHERE birthday = (SELECT MAX(birthday) FROM student);
+--------+------------+
| name | birthday |
+--------+------------+
| 陈刚 | 2006-05-30 |
+--------+------------+
1 row in set (0.00 sec)

⚠️ 标量子查询必须恰好返回一个值。如果子查询查出了多行,外层的 => 就不知道跟谁比,直接报错:ERROR 1242 (21000): Subquery returns more than 1 row

三、列子查询:多行一列,配 IN

需求:查一班学生的所有成绩

第一步"一班有哪些学生"返回的是多个 id(1、2、5),一个值装不下,这时用 IN 接住整列结果:

SELECT student_id, course_id, score
FROM score
WHERE student_id IN (SELECT id FROM student WHERE class_id = 1);
+------------+-----------+-------+
| student_id | course_id | score |
+------------+-----------+-------+
| 1 | 1 | 90.0 |
| 1 | 2 | 85.0 |
| 1 | 3 | 88.0 |
| 2 | 1 | 76.0 |
| 2 | 2 | 92.0 |
| 5 | 2 | 73.0 |
| 5 | 3 | NULL |
+------------+-----------+-------+
7 rows in set (0.00 sec)

子查询先算出一班学生 id 列表 (1, 2, 5),外层等价于 WHERE student_id IN (1, 2, 5),命中张三 3 条、李四 2 条、陈刚 2 条,共 7 行。

反过来用 NOT IN 就是"不在名单里",比如查二班学生的成绩记录数等,思路相同。

四、行子查询:一行多列(了解即可)

行子查询一次返回"一行里的多个列",可以整行比较。比如"找出和张三性别相同、且同班的学生":

SELECT name, gender, class_id
FROM student
WHERE (gender, class_id) = (SELECT gender, class_id FROM student WHERE name = '张三');
+--------+--------+----------+
| name | gender | class_id |
+--------+--------+----------+
| 张三 | 男 | 1 |
| 李四 | 男 | 1 |
| 陈刚 | 男 | 1 |
+--------+--------+----------+
3 rows in set (0.00 sec)

(gender, class_id) = ('男', 1) 相当于两个条件同时相等。实际开发中出场率不高,见到能认出来即可。

五、子查询放在 FROM 中:派生表

子查询的结果是"多行多列"时,它本身就长得像一张表——那就可以放在 FROM 后面,当成临时表来查。这种表叫派生表

需求:查出"平均分不低于 80 的学生"的 id 和平均分。平均分是分组算出来的结果,想再对它做筛选和加工,可以先把"每人平均分"做成派生表:

SELECT t.student_id, t.avg_score
FROM (SELECT student_id, AVG(score) AS avg_score
FROM score
GROUP BY student_id) AS t
WHERE t.avg_score >= 80;
+------------+-----------+
| student_id | avg_score |
+------------+-----------+
| 1 | 87.66667 |
| 2 | 84.00000 |
+------------+-----------+
2 rows in set (0.00 sec)

验算:张三 (90+85+88)÷3 = 87.66667;李四 (76+92)÷2 = 84;王小红 75、赵敏 69.5、陈刚 73(NULL 被 AVG 忽略),都不到 80。

派生表必须起别名(上面的 AS t),哪怕你后面根本用不到这个名字。漏写会报错:

ERROR 1248 (42000): Every derived table must have its own alias

可以这样理解:MySQL 把子查询结果当成一张真的表来对待,而表总得有个名字才能被引用。

六、EXISTS:只问"有没有"

EXISTS 不关心子查询查出了什么,只关心能不能查到至少一行:查得到就成立,查不到就不成立。常用于"存在某某记录的……"这类需求。

例:查出"至少有一门课达到 90 分"的学生:

SELECT s.id, s.name
FROM student AS s
WHERE EXISTS (SELECT 1 FROM score AS sc
WHERE sc.student_id = s.id AND sc.score >= 90);
+----+-----------+
| id | name |
+----+-----------+
| 1 | 张三 |
| 2 | 李四 |
| 3 | 王小红 |
+----+-----------+
3 rows in set (0.00 sec)

张三有 90、李四有 92、王小红有 95,成立;赵敏最高 81、陈刚最高 73,不成立。

注意这个子查询里用到了外层的 s.id——外层每检查一个学生,子查询就带着这个学生的 id 跑一次,这种"里面引用外面"的子查询叫相关子查询SELECT 1 是习惯写法:既然只看有没有行,查什么列都无所谓,写 1 最省事。NOT EXISTS 则是"不存在……的"。

七、子查询 vs 连接查询:怎么选

很多需求两种写法都行。比如"查一班学生的所有成绩"用连接写:

SELECT sc.student_id, sc.course_id, sc.score
FROM score AS sc
INNER JOIN student AS s ON sc.student_id = s.id
WHERE s.class_id = 1;

结果和第三节的 IN 子查询完全一样(7 行)。选择建议:

场景建议
结果里需要显示多张表的列(姓名+课程名)只能用 JOIN
条件依赖一个统计值(高于平均分)标量子查询最自然
只用另一张表做"名单过滤",不显示它的列IN / EXISTS 子查询更直观
对分组统计结果再加工FROM 派生表

经验法则:能用 JOIN 尽量用 JOIN——大多数情况下优化器对 JOIN 处理得更好,而且信息都在一个平面上,方便扩展列。但当子查询能让 SQL 像大白话一样"先算 X 再比 X"时,直观易读也是重要的价值,不必教条。

⚠️ 新手常见坑

  1. 标量子查询返回了多行= / > / < 后面的子查询必须恰好一行一列,多行会报 ERROR 1242。拿不准就改用 IN。
  2. FROM 里的派生表忘记起别名:必报 ERROR 1248(Every derived table must have its own alias),无一例外。
  3. NOT IN 遇上 NULL 全军覆没:如果子查询结果里含 NULL,NOT IN 会一行都查不出来(NULL 比较的老问题)。子查询里记得加 WHERE 列 IS NOT NULL,或改用 NOT EXISTS。
  4. 括号漏写:子查询必须整个包在圆括号里,WHERE score > SELECT AVG(score)... 是语法错误。
  5. 什么都套子查询:三层四层嵌套的 SQL 很难读也难优化,先想想能不能用一个 JOIN + GROUP BY 说清楚。

📝 小结

  • 子查询 = 查询套查询,先执行内层,再把结果交给外层;必须写在圆括号里。
  • 标量子查询返回单个值,配 = > < 等比较;列子查询返回一列多行,配 IN / NOT IN。
  • 行子查询整行比较,了解即可;子查询放 FROM 里叫派生表,必须起别名。
  • EXISTS 只判断"子查询有没有结果",常写成带外层引用的相关子查询。
  • 选择原则:要显示多表的列或追求性能优先 JOIN;"先算一个值再比较"这类需求用子查询更直观。

✍️ 练习题

  1. 查出低于全校平均分的成绩(显示 student_id、course_id、score)。
参考答案
SELECT student_id, course_id, score
FROM score
WHERE score < (SELECT AVG(score) FROM score);

平均分 78.90909,低于它的 5 条:李四数学 76.0、王小红英语 60.0、王小红计算机 70.0、赵敏数学 58.0、陈刚英语 73.0。陈刚的 NULL 不参与比较,不会出现。

  1. 用 IN 子查询,查出二班学生的所有成绩记录。
参考答案
SELECT student_id, course_id, score
FROM score
WHERE student_id IN (SELECT id FROM student WHERE class_id = 2);

二班是王小红(3)、赵敏(4),共 5 行:(3,1,95.0)、(3,2,60.0)、(3,3,70.0)、(4,1,58.0)、(4,3,81.0)。

  1. 查出数学考了最高分的学生姓名(提示:先用标量子查询算数学最高分)。
参考答案
SELECT s.name, sc.score
FROM score AS sc
INNER JOIN student AS s ON sc.student_id = s.id
WHERE sc.course_id = 1
AND sc.score = (SELECT MAX(score) FROM score WHERE course_id = 1);

数学最高分 95.0,是王小红。

  1. 用 FROM 派生表,查出"各科平均分"中的最高值。
参考答案
SELECT MAX(t.avg_score) AS 最高科目均分
FROM (SELECT course_id, AVG(score) AS avg_score
FROM score
GROUP BY course_id) AS t;

三科平均分为 79.75000、77.50000、79.66667,最高是 79.75000(数学)。别忘了别名 t。

  1. 用 NOT EXISTS 查出"没有任何一门课达到 85 分"的学生姓名。
参考答案
SELECT s.name
FROM student AS s
WHERE NOT EXISTS (SELECT 1 FROM score AS sc
WHERE sc.student_id = s.id AND sc.score >= 85);

赵敏(最高 81.0)和陈刚(最高 73.0)。张三、李四、王小红都有 85 分以上的成绩,被排除。


⬅ 上一节 | 🏠 返回目录 | 下一节 ➡