数据库系统概论(十)SQL 嵌套查询 超详细讲解(附带例题表格对比带你一步步掌握)
- 前言
- 一、什么是嵌套查询?
-
- [1. 基础组成:查询块](#1. 基础组成:查询块)
- [2. 嵌套的两种常见位置](#2. 嵌套的两种常见位置)
-
- [(1)藏在 FROM 子句里(当子查询是一张"临时表")](#(1)藏在 FROM 子句里(当子查询是一张“临时表”))
- [(2)藏在 WHERE 子句里(用子查询结果作条件)](#(2)藏在 WHERE 子句里(用子查询结果作条件))
- [3. 多层嵌套](#3. 多层嵌套)
- [4. 子查询的限制:不能用 ORDER BY](#4. 子查询的限制:不能用 ORDER BY)
- [二、带有 IN 谓词的子查询](#二、带有 IN 谓词的子查询)
-
- [1. IN谓词的核心作用](#1. IN谓词的核心作用)
- [2. 子查询 vs 连接查询](#2. 子查询 vs 连接查询)
- [3. 不相关子查询](#3. 不相关子查询)
- [4. IN子查询的常见用法场景](#4. IN子查询的常见用法场景)
- 三、带有比较运算符的子查询
- 四、带有ANY(SOME)或ALL谓词的子查询
-
- [1. 什么是ANY和ALL?](#1. 什么是ANY和ALL?)
- [2. ANY/ALL怎么用?](#2. ANY/ALL怎么用?)
- [3. 经典案例](#3. 经典案例)
- [4. ANY/ALL和聚集函数的等价表](#4. ANY/ALL和聚集函数的等价表)
- [五、带有 EXISTS 谓词的子查询](#五、带有 EXISTS 谓词的子查询)
-
- [1. EXISTS谓词的基本概念](#1. EXISTS谓词的基本概念)
- [2. 经典案例](#2. 经典案例)
- [3. EXISTS与IN的对比](#3. EXISTS与IN的对比)
- [4. 用EXISTS实现全称量词查询](#4. 用EXISTS实现全称量词查询)
前言
- 在前几期博客中,我们探讨了 SQL 连接查询和单表查询技术。
- 从本节开始,我们将深入讲解 SQL 中嵌套查询的知识点。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343我的数据库系统概论专栏
https://blog.csdn.net/2402_83322742/category_12911520.html?spm=1001.2014.3001.5482
一、什么是嵌套查询?
核心概念 :
嵌套查询就像"大问题拆分成小问题",把一个查询(小问题)藏在另一个查询(大问题)里,层层解决。
1. 基础组成:查询块
-
查询块 :一个完整的
SELECT-FROM-WHERE
语句,比如:sqlSELECT Sno FROM SC WHERE Cno='81001' -- 小问题:查选了81001课程的学号
-
嵌套 :把小问题的结果,当作大问题的条件。大问题叫 外层查询(父查询) ,小问题叫 内层查询(子查询)。
2. 嵌套的两种常见位置
(1)藏在 FROM 子句里(当子查询是一张"临时表")
示例:
sql
SELECT Sname -- 大问题:查学生姓名
FROM Student,
(SELECT Sno FROM SC WHERE Cno='81001') AS SC1 -- 小问题:先查选了81001课的学号,当作临时表SC1
WHERE Student.Sno=SC1.Sno; -- 用临时表SC1的学号,匹配学生表找姓名
逻辑 :先得到选了81001课的学号列表(SC1),再从学生表中找到这些学号对应的姓名。
(2)藏在 WHERE 子句里(用子查询结果作条件)
示例:
sql
SELECT Sname -- 大问题:查学生姓名
FROM Student
WHERE Sno IN (SELECT Sno FROM SC WHERE Cno='81002'); -- 小问题:查选了81002课的学号,用IN判断是否在其中
逻辑 :先查选了81002课的学号,再在学生表中筛选出学号在这个列表里的学生姓名。
3. 多层嵌套
特点 :子查询里还可以再套子查询(最多嵌套多少层?SQL标准没限制,但实际别写太复杂),像剥洋葱一样层层查询
示例:
sql
SELECT Sname -- 大问题:查"所有课程都选了"的学生姓名
FROM Student
WHERE NOT EXISTS ( -- 外层:不存在"该学生没选的课程"
SELECT *
FROM Course
WHERE NOT EXISTS ( -- 内层:不存在"该课程没被该学生选"
SELECT *
FROM SC
WHERE Sno= Student.Sno -- 学生学号
AND Cno= Course.Cno -- 课程编号
)
);
逻辑:
- 外层
NOT EXISTS
:找"不存在没选的课程"的学生。 - 内层
NOT EXISTS
:对每门课程,检查"学生是否没选这门课"。 - 两层结合:如果所有课程都被学生选了(内层都不存在没选的情况),外层就会选中该学生。
4. 子查询的限制:不能用 ORDER BY
-
原因:子查询只是给外层提供数据(比如学号列表),不需要排序,排序留给外层做。
-
正确做法:
sqlSELECT Sname FROM Student WHERE Sno IN ( SELECT Sno FROM SC WHERE Cno='81002' -- 内层不排序 ) ORDER BY Sname; -- 排序放在外层
二、带有 IN 谓词的子查询
1. IN谓词的核心作用
一句话理解:
IN
就像"在名单里",判断某个值是否在子查询返回的集合中。
场景举例:
想查"选了'信息系统概论'这门课的学生",可以分两步:
- 先查这门课的课程号(比如81004)→ 得到一个"课程号名单"。
- 再查选了这个课程号的学生学号 → 得到"学号名单"。
- 最后从学生表中找出学号在"学号名单"里的学生 → 用
IN
判断是否在名单中。
2. 子查询 vs 连接查询
相同目标:查选修"信息系统概论"的学生学号和姓名。
(1)连接查询(表关联法)
sql
SELECT Sno, Sname
FROM Student
JOIN SC ON Student.Sno = SC.Sno -- 学生表和选课表用学号关联
JOIN Course ON SC.Cno = Course.Cno -- 选课表和课程表用课程号关联
WHERE Course.Cname = '信息系统概论'; -- 直接通过表关联找到对应课程的学生
逻辑:把三张表"拼"在一起,像拼图一样直接找到符合条件的行。
(2)子查询(分步筛选法)
sql
SELECT Sno, Sname
FROM Student
WHERE Sno IN ( -- 学号是否在"选了这门课的学号名单"里?
SELECT Sno FROM SC
WHERE Cno IN ( -- 课程号是否在"信息系统概论的课程号名单"里?
SELECT Cno FROM Course WHERE Cname='信息系统概论' -- 先查课程号
)
);
逻辑:
- 最内层:先查"信息系统概论"的课程号(比如81004)→ 得到课程号名单。
- 中间层:再查选了这个课程号的学号 → 得到学号名单。
- 最外层:从学生表中筛选学号在名单里的学生 → 用
IN
做判断。
对比总结:
- 连接查询:适合"直接拼表关联"的简单场景,性能可能更高。
- 子查询:适合"分步解决问题"的逻辑,思路更清晰,尤其适合多层筛选。
3. 不相关子查询
定义 :子查询的条件不依赖父查询 ,可以独立运行,结果直接给父查询用。
特点:
- 子查询先执行,结果是一个固定的集合(比如学号列表、课程号列表)。
- 父查询再用这个集合做条件筛选。
示例:查平均分高于85分的学生
sql
SELECT Sno, Sname, Smajor -- 父查询:从学生表找学生
FROM Student
WHERE Sno IN (
SELECT Sno FROM SC -- 子查询:先查平均分>85的学号
GROUP BY Sno -- 按学号分组,算每个学生的平均分
HAVING AVG(grade) > 85
);
执行步骤:
- 子查询独立运行:按学号分组,算出平均分>85的学号列表(如{001, 003})。
- 父查询用
IN
判断学生学号是否在列表里,返回结果。
4. IN子查询的常见用法场景
场景描述 | 示例SQL(简化版) |
---|---|
查选了某课程的学生 | SELECT 姓名 FROM 学生 WHERE 学号 IN (SELECT 学号 FROM 选课 WHERE 课程号='81002') |
查某类课程的学生信息 | 多层IN嵌套,先查课程号,再查学号 |
查分组统计后的结果(如平均分) | SELECT ... WHERE 学号 IN (SELECT 学号 FROM 选课 GROUP BY 学号 HAVING AVG(成绩)>85) |
三、带有比较运算符的子查询
1. 带比较运算符的子查询是什么?
核心思路 :子查询的结果是一个单一值 (比如一个数字、一个名字),这时可以用 >
<
=
等比较运算符,把这个单一值和父查询的数据做对比。
举个例子:
想查和"刘晨"同一个专业的学生。
-
子查询先找出刘晨的专业:
SELECT Smajor FROM Student WHERE Sname = '刘晨'
,结果是一个值(比如"计算机")。 -
父查询用
=
比较,找出专业等于这个值的学生:sqlSELECT Sno, Sname, Smajor FROM Student WHERE Smajor = (子查询结果)
注意!
子查询必须跟在比较符后面 ,不能写反 !
❌ 错误写法:WHERE (子查询) = Smajor
✅ 正确写法:WHERE Smajor = (子查询)
2. 子查询的分类
(1)不相关子查询
-
特点 :子查询的条件不依赖父查询,可以先独立运行子查询,再把结果传给父查询。
-
执行顺序:先算子查询 → 得到结果 → 再用结果执行父查询。
-
例子 :查刘晨同系的学生(子查询只需要查一次"刘晨的系")。
sqlSELECT Sno, Sname, Sdept FROM Student WHERE Sdept = (SELECT Sdept FROM Student WHERE Sname = '刘晨')
(2)相关子查询
-
特点 :子查询的条件依赖父查询当前行的数据,需要父查询和子查询"联动"执行。
-
执行顺序 :
- 父查询先取第一行数据,把相关值(比如学号)传给子查询;
- 子查询用这个值计算结果(比如该学号的平均分);
- 父查询用结果判断当前行是否符合条件,符合就保留;
- 重复步骤1-3,直到处理完所有行。
-
例子 :查每个学生超过自己平均分的课程成绩。
sqlSELECT Sno, Cno FROM SC x -- x是外层表的别名 WHERE Grade >= ( SELECT AVG(Grade) FROM SC y -- y是内层表的别名 WHERE y.Sno = x.Sno -- 内层用x的学号找对应数据 )
通俗理解 :
- 先看第一个学生(比如学号20180001),子查询算出他的平均分是89;
- 父查询检查他的每门课成绩是否≥89,符合条件的就选出来;
- 再处理下一个学生,重复这个过程。
3. 经典案例:查每门课的最高分学生
需求:找出每门课成绩最高的学生的学号、课程号和成绩。
SQL写法:
sql
SELECT sno, cno, grade
FROM sc x
WHERE grade = (
SELECT MAX(grade)
FROM sc y
WHERE y.cno = x.cno -- 内层根据外层的课程号(x.cno)找最高分
)
执行逻辑:
- 外层先取第一行数据(比如课程81001,学号20180001,成绩85);
- 子查询根据课程号81001,算出该课程的最高分是85;
- 比较当前成绩85是否等于最高分85,等于就保留;
- 处理下一行(比如课程81002,学号20180002,成绩98),子查询算出该课程最高分98,符合条件,保留;
- 以此类推,直到所有行处理完。
四、带有ANY(SOME)或ALL谓词的子查询
1. 什么是ANY和ALL?
-
ANY = 任意一个 (只要满足集合中的某一个值)
-
类似"至少有一个行符合条件"。
-
ALL = 所有 (必须满足集合中的每一个值)
-
类似"所有行都要符合条件"。
举个生活例子:
- 如果你说"我要选一门 比ANY数学课分数高 的课" → 只要有一门数学课分数比它低就行;
- 如果你说"我要选一门 比ALL数学课分数高 的课" → 必须所有数学课分数都比它低!
2. ANY/ALL怎么用?
格式 :
比较运算符 + ANY/ALL + (子查询)
比如:
> ANY
:大于子查询结果中的某个值< ALL
:小于子查询结果中的所有值
常见搭配表:
需求 | 用ANY/ALL | 等价于聚集函数 | 例子 |
---|---|---|---|
等于集合中某个值 | = ANY |
IN |
Sno IN (子查询) 等价于 Sno = ANY (子查询) |
不等于集合中所有值 | <> ALL |
NOT IN |
Sno NOT IN (子查询) 等价于 Sno <> ALL (子查询) |
大于集合中最小的 | > ANY |
> MIN |
比任意一个值大 → 只要大于最小值就行 |
大于集合中最大的 | > ALL |
> MAX |
比所有值大 → 必须大于最大值 |
小于集合中最大的 | < ANY |
< MAX |
比某个值小 → 只要小于最大值就行 |
小于集合中最小的 | < ALL |
< MIN |
比所有值小 → 必须小于最小值 |
3. 经典案例
场景 :学生表中有计算机专业和其他专业,想找非计算机专业中年龄相关的学生。
表结构:
Sname | Smajor | Sbirthdate |
---|---|---|
张三 | 计算机 | 2000-01-01 |
李四 | 信息安全 | 2001-03-08 |
王五 | 计算机 | 2000-12-31 |
赵六 | 数据科学 | 2002-06-12 |
案例1:查非计算机专业中,比计算机专业任意一个学生年龄小的学生(出生日期更晚)
需求翻译 :只要比计算机专业里至少一个人年龄小(即出生日期晚于计算机专业的某个人)。
sql
SELECT Sname, Smajor, Sbirthdate
FROM Student
WHERE Smajor <> '计算机' -- 排除计算机专业
AND Sbirthdate > ANY (
SELECT Sbirthdate FROM Student WHERE Smajor = '计算机'
);
执行逻辑:
- 子查询先找出计算机专业的所有出生日期:
2000-01-01
和2000-12-31
; > ANY
表示只要大于其中任意一个值 就行 → 比如赵六的2002-06-12
大于2000-01-01
,符合条件。
等价写法 :用MIN
聚集函数
sql
SELECT ...
WHERE Sbirthdate > (SELECT MIN(Sbirthdate) FROM Student WHERE Smajor='计算机');
(因为只要大于最小值,必然大于某个值)
案例2:查非计算机专业中,比计算机专业所有学生年龄都小的学生(出生日期更晚)
需求翻译 :必须比计算机专业里所有人年龄都小(出生日期晚于计算机专业的所有人)。
sql
SELECT Sname, Smajor, Sbirthdate
FROM Student
WHERE Smajor <> '计算机'
AND Sbirthdate > ALL (
SELECT Sbirthdate FROM Student WHERE Smajor = '计算机'
);
执行逻辑:
> ALL
表示必须大于所有值 → 计算机专业最大的出生日期是2000-12-31
,只有赵六的2002-06-12
大于它,所以只有赵六符合。
等价写法 :用MAX
聚集函数
sql
SELECT ...
WHERE Sbirthdate > (SELECT MAX(Sbirthdate) FROM Student WHERE Smajor='计算机');
(必须大于最大值,才能大于所有值)
4. ANY/ALL和聚集函数的等价表
需求 | 用ANY/ALL | 等价的聚集函数写法 |
---|---|---|
大于某个值 | > ANY |
> MIN |
大于所有值 | > ALL |
> MAX |
小于某个值 | < ANY |
< MAX |
小于所有值 | < ALL |
< MIN |
等于某个值(IN) | = ANY |
IN |
不等于所有值(NOT IN) | <> ALL |
NOT IN |
五、带有 EXISTS 谓词的子查询
1. EXISTS谓词的基本概念
核心作用 :判断子查询是否有结果 ,返回的是true
或者false
,并不关注子查询的具体内容。
- 当
EXISTS
后的子查询能查出数据(结果不为空)时,就返回true
,此时外层查询的条件得以成立; - 若子查询没有结果(结果为空),则返回
false
,外层查询的条件不成立。
语法格式:
sql
WHERE EXISTS (子查询);
WHERE NOT EXISTS (子查询); -- 子查询为空时返回true
特别说明:
- 子查询通常用
SELECT *
,这是因为我们只关心子查询有没有结果,而不关心具体查出来的内容是什么。
2. 经典案例
案例1:查询选修了81001号课程的学生姓名。
使用EXISTS的写法:
sql
SELECT Sname
FROM Student
WHERE EXISTS (
SELECT *
FROM SC
WHERE Sno = Student.Sno AND Cno = '81001'
);
执行步骤:
- 外层查询从
Student
表中取出一行数据,获取当前学生的学号,比如20180001
; - 子查询开始执行,查看
SC
表中是否存在学号为20180001
且课程号为81001
的记录; - 若存在这样的记录,子查询返回
true
,那么这个学生的姓名就会被添加到结果中; - 若不存在,子查询返回
false
,该学生的姓名不会出现在结果里; - 重复上述步骤,直到处理完
Student
表中的所有行。
案例2:查询没有选修81001号课程的学生姓名。
使用NOT EXISTS的写法:
sql
SELECT Sname
FROM Student
WHERE NOT EXISTS (
SELECT *
FROM SC
WHERE Sno = Student.Sno AND Cno = '81001'
);
关键逻辑:
- 当子查询没有结果时,
NOT EXISTS
返回true
,意味着这个学生没有选修该课程。
3. EXISTS与IN的对比
这两种方式在很多情况下可以实现相同的查询效果,但也存在一些差异。
场景 | EXISTS | IN |
---|---|---|
查询选修了课程的学生 | WHERE EXISTS (SELECT * FROM SC WHERE SC.Sno = Student.Sno) |
WHERE Sno IN (SELECT Sno FROM SC) |
适用情况 | 适合子查询结果集较大的情况,它是通过判断是否存在来筛选的 | 适合子查询结果集较小的情况,它需要先计算出子查询的所有结果 |
4. 用EXISTS实现全称量词查询
在SQL里没有直接表示全称量词(∀
)的操作符,不过可以借助NOT EXISTS
来间接实现。
案例:查询选修了全部课程的学生姓名。
需求理解 :要找出这样的学生,对于所有课程而言,他们都有选修记录。
SQL写法:
sql
SELECT Sname
FROM Student
WHERE NOT EXISTS (
-- 查找一门课程
SELECT *
FROM Course
WHERE NOT EXISTS (
-- 检查该学生是否选修了这门课
SELECT *
FROM SC
WHERE Sno = Student.Sno AND Cno = Course.Cno
)
);
逻辑拆解:
- 从外层看,对于每个学生,先假设存在一门他没选修的课程;
- 中间的子查询负责遍历所有课程;
- 最内层的子查询去验证该学生是否选修了这门课程;
- 如果对于某门课程,学生没有选修记录,那么
NOT EXISTS
就会返回false
,这个学生就不符合要求; - 只有当所有课程都被学生选修了,也就是对于所有课程,内层子查询都能查到记录,
NOT EXISTS
才会返回true
,这个学生才会被查询出来。
以上就是这篇博客的全部内容,下一篇我们将继续探索更多精彩内容。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343我的数据库系统概论专栏
https://blog.csdn.net/2402_83322742/category_12911520.html?spm=1001.2014.3001.5482
|--------------------|
| 非常感谢您的阅读,喜欢的话记得三连哦 |
