9. 子查询(嵌套查询)

# MySQL 子查询(Subquery)全解析
/*
子查询(也叫嵌套查询)是指在一个 SQL 语句(主查询 / 外部查询)中嵌套另一个 SQL 语句(子查询 / 内部查询),子查询的结果会作为主查询的条件、数据源、计算依据 等。子查询的核心优势是逻辑清晰、易读,适合将复杂问题拆解为简单的嵌套逻辑;但需注意性能优化,避免大数据量下的效率问题。
*/
# 一、子查询的核心特性
/*
语法要求:子查询必须用 () 包裹,这是 MySQL 的强制语法;
执行逻辑:
非关联子查询:先执行子查询,将结果传给主查询(仅执行一次);
关联子查询:主查询每遍历一行,子查询执行一次(依赖主查询的列);
返回结果:子查询可返回标量(单个值)、列(单列多行)、行(单行多列)、表(多行多列),不同结果对应不同的使用场景。
*/
# 二、子查询的分类(按返回结果类型)

# 1. 标量子查询(Scalar Subquery)
-- 定义:返回单行单列的单个值(如数字、字符串);
-- 适用场景:可直接用在 WHERE、SELECT、HAVING 等子句中,搭配 =、>、<、>=、<= 等标量运算符;



-- 1. WHERE子句:查询数学成绩高于 钱电 的学生名单

-- 1步:先查询出钱电的数学成绩
sql
select score from sc join student 
on sc.sno=student.sno 
join course on course.cno=sc.cno 
where student.sname='钱电' and course.cname='数学'; 
-- 2步: 查询数学成绩>60的学生名单
sql
SELECT sname,score 
from sc join student 
on sc.sno=student.sno 
join course on course.cno=sc.cno
WHERE  score > 60 and course.cname='数学' ; 
-- 3步: 将以上两个语句合并在一起执行
sql
SELECT sname,score 
from sc join student 
on sc.sno=student.sno 
join course on course.cno=sc.cno
WHERE  score > (select score from sc join student 
on sc.sno=student.sno 
join course on course.cno=sc.cno 
where student.sname='钱电' and course.cname='数学') and course.cname='数学' ;
-- 2. SELECT子句:查询每个学生的姓名和数学成绩(无成绩则返回NULL)
sql
SELECT s.sno,
  s.sname,
  (SELECT sc.score FROM sc join course on sc.cno=course.Cno WHERE sc.sno = s.sno AND course.cname = '数学' ) AS 数学成绩 -- 先查询所有学生的数学成绩
FROM student s
ORDER BY s.sno;
# 2. 列子查询(Column Subquery)
/*
定义:返回单列多行的结果集(集合);
适用场景:搭配 IN、NOT IN、ANY/SOME、ALL 等集合运算符;
核心运算符说明:
IN:匹配集合中任意一个值;
ANY/SOME:满足集合中任意一个条件(如 score > ANY(...));
ALL:满足集合中所有条件(如 score > ALL(...))。
*/
-- 1. IN:查询班级 01 所有学生的成绩
sql
SELECT * FROM sc 
WHERE sno IN (SELECT sno FROM student WHERE class = '01');
-- 2. ANY:查询数学成绩高于班级 01 任意一个学生数学成绩的学生姓名,班级和数学成绩
-- 1步:查询班级 01 所有学生的数学成绩
sql
SELECT score FROM sc join course on sc.Cno=course.cno 
WHERE sno IN (SELECT sno FROM student WHERE class = '01') and course.Cname='数学';
-- 2步:查询所有学生数学成绩大于30的学生姓名,班级和数学成绩
sql
SELECT sname,class,score 
from sc join course on sc.Cno=course.Cno
join student on sc.Sno=student.Sno 
WHERE course.Cname='数学' and score>30; 
-- 3步:将以上两步合并
sql
SELECT sname,class,score 
from sc join course on sc.Cno=course.Cno
join student on sc.Sno=student.Sno 
WHERE course.Cname='数学' and score>
any(SELECT score FROM sc join course on sc.Cno=course.cno 
WHERE sno IN (SELECT sno FROM student WHERE class = '01') and course.Cname='数学'); 
-- 3. ALL:查询数学成绩高于班级01所有学生数学成绩的记录
sql
SELECT sname,class,score 
from sc join course on sc.Cno=course.Cno
join student on sc.Sno=student.Sno 
WHERE course.Cname='数学' and score>
all(SELECT score FROM sc join course on sc.Cno=course.cno 
WHERE sno IN (SELECT sno FROM student WHERE class = '01') and course.Cname='数学');
# 3. 行子查询(Row Subquery)
/*
定义:返回单行多列(或多行多列)的结果集,需用行构造器 (col1, col2) 匹配;
适用场景:多列条件同时匹配(如 (subject, score) = (子查询))。
*/
-- 1.查询01班16岁的学生
sql
SELECT * FROM student 
WHERE (class, sage) = (('01',16));
-- 2. 查询和赵雷语文成绩相同的学生姓名和语文成绩(cname+score)
sql
SELECT sname, score 
FROM student  JOIN sc ON student.sno = sc.sno
join course on sc.Cno=course.Cno  
WHERE (course.Cname, sc.score) = (
  SELECT cname, score 
  FROM sc join course on sc.Cno=course.Cno
  WHERE sno = (SELECT sno FROM student WHERE sname = '赵雷') 
    AND cname = '语文');
-- 结果:赵雷(80)、孙风(80)

-- 2. 行集合匹配:

-- 查询01班武汉的男生或02班北京的女生
sql
SELECT * FROM student 
WHERE (class, sbirthplace,ssex) IN (('01','武汉','男'),('02','北京','女'));
# 4. 表子查询(Table Subquery)
/*
定义:返回多行多列的结果集(相当于临时表);
适用场景:用在 FROM 子句中(必须给临时表加别名),或 JOIN 中;
注意:FROM 子句中的子查询必须指定别名,否则 MySQL 报错。
*/
-- 1. 统计每个学生总分>160的学生
-- 1步:算出每个学生的总分
sql
SELECT student.sno,sname,sum(score) 总分
from student join sc on student.Sno=sc.Sno 
GROUP BY student.Sno;
-- 2步:将上面的结果作为一个表来处理
sql
SELECT sname, 总分 
FROM (
  SELECT student.sno,sname,sum(score) 总分 
from student join sc on student.Sno=sc.Sno 
GROUP BY student.Sno) AS t -- 临时表别名t
WHERE 总分 > 160; 
# 三、子查询的分类(按关联方式)
/*
1. 非关联子查询(Non-Correlated Subquery)
定义:子查询不依赖主查询的列,可独立执行,仅执行一次;
特点:性能较好,优先使用;
示例:前面的标量子查询、列子查询(如 IN 示例)均为非关联子查询。
2. 关联子查询(Correlated Subquery)
定义:子查询依赖主查询的列(如 sc.stu_id = s.id),主查询每遍历一行,子查询执行一次;
核心运算符:EXISTS/NOT EXISTS(判断子查询是否有结果,返回布尔值);
特点:逻辑灵活,但大数据量下性能较差(需优化)。
*/
-- 1. EXISTS:查询有数学成绩的学生学号,姓名
sql
SELECT sno,sname FROM student
WHERE EXISTS (       -- exists 判断是否存在某结果
  SELECT * 
  FROM sc join course on sc.Cno=course.Cno
  WHERE sc.sno = student.sno AND course.cname = '数学')
ORDER BY sno;
-- 2. NOT EXISTS:查询没有数学成绩的学生姓名
sql
SELECT sno,sname FROM student
WHERE not EXISTS (    -- 和exists相反,是否不存在某结果
  SELECT 1 -- 无需返回具体列,1仅占位,效率更高
  FROM sc join course on sc.Cno=course.Cno
  WHERE sc.sno = student.sno AND course.cname = '数学')
	ORDER BY sno;
-- 3. HAVING中使用关联子查询:查询班级数学平均分高于全校数学平均分的班级
sql
SELECT class, AVG(sc.score) AS class_avg 
FROM student  JOIN sc ON student.sno = sc.sno 
join course on sc.Cno=course.Cno
WHERE course.Cname = '数学' 
GROUP BY class 
HAVING AVG(sc.score) > (SELECT AVG(score) FROM sc join course on sc.Cno=course.Cno WHERE cname = '数学');
四、子查询的常见坑与优化技巧

1. 避免 NOT IN 陷阱(NULL 值影响)
NOT IN 遇到子查询结果包含 NULL 时,会返回空集(因为 NULL 的比较结果是 UNKNOWN),建议用 NOT EXISTS 替代:
sql
-- 错误示例(若score.stu_id有NULL,结果为空)
sql
SELECT name FROM student WHERE id NOT IN (SELECT stu_id FROM score WHERE subject = '语文');
-- 正确示例(不受NULL影响,且性能更好)
sql
SELECT name FROM student s 
WHERE NOT EXISTS (
  SELECT 1 FROM score sc WHERE sc.stu_id = s.id AND sc.subject = '语文'
);
2. 关联子查询优化为 JOIN
关联子查询(尤其是 EXISTS)可替换为 JOIN,通常效率更高(JOIN 利用索引,避免逐行执行子查询):
sql
-- 原EXISTS查询
sql
SELECT name FROM student s 
WHERE EXISTS (SELECT 1 FROM score sc WHERE sc.stu_id = s.id AND sc.subject = '数学');
-- 优化为JOIN(DISTINCT去重)
sql
SELECT DISTINCT s.name 
FROM student s JOIN score sc ON s.id = sc.stu_id 
WHERE sc.subject = '数学';
/*
3. 表子查询加索引 / 简化嵌套
子查询的关联列(如 sno)加索引,减少扫描;
多层嵌套子查询拆分为临时表或 JOIN,降低执行复杂度;
子查询结果集过大时,用 LIMIT 限制行数(仅适用于业务允许的场景)。
4. 优先用标量子查询替代列子查询
标量子查询执行效率更高,若业务逻辑允许,优先选择标量结果。
五、子查询的适用场景总结
  场景	      推荐子查询类型	     替代方案
单值条件过滤	标量子查询	         直接 JOIN
多值条件过滤	列子查询(IN/ANY)	  JOIN + IN
多列条件匹配	行子查询	多条件      AND
临时表统计	  表子查询(FROM)	    临时表 / CTE
存在性判断	  关联子查询(EXISTS)	JOIN + DISTINCT
六、核心总结
子查询是 “嵌套查询”,逻辑清晰但需注意性能;
按返回结果分:标量、列、行、表子查询;按关联方式分:关联 / 非关联子查询;
EXISTS 比 NOT IN 更可靠(避免 NULL 陷阱),JOIN 比关联子查询更高效;
子查询必须用 () 包裹,FROM 子句中的表子查询必须加别名。
实际开发中,优先用 JOIN 替代复杂关联子查询,仅在逻辑极复杂、JOIN 难以实现时使用子查询,兼顾可读性和性能。
*/