文章目录
- 前言
- 数据表
- 一、子查询(subquery)
- 二、不相关子查询(unrelated subqueries)
- 1.概念
- 2.查询逻辑
- 三、相关子查询(related subqueries)
- 1.概念
- 2.查询逻辑
- 3.带有EXISTS谓词的子查询
- 总结
前言
开篇感言
第一次发文章,源于今天下午上课时,相关子查询讲得台下学生一脸懵逼。为了不误人子弟与自己卑微的尊严,故有此文 。如果还没懂,随时找我,欢迎留言。
一些说明
本文所用概念与示例大多引用自王珊老师的《数据库系统概论》第5版,部分原创。
以下是本篇文章正文内容
数据表
1、Student(学生表)
Sno | Sname | Ssex | Sage | Sdept |
---|---|---|---|---|
201215121 | 李勇 | 男 | 20 | CS |
201215122 | 刘晨 | 女 | 19 | CS |
201215123 | 王敏 | 女 | 18 | MA |
201215125 | 张立 | 男 | 19 | IS |
2、Course(课程表)
Cno | Cname | Cpno | Ccredit |
---|---|---|---|
1 | 数据库 | 5 | 4 |
2 | 数学 | 2 | |
3 | 信息系统 | 1 | 4 |
4 | 操作系统 | 6 | 3 |
5 | 数据结构 | 7 | 4 |
6 | 数据处理 | 2 | |
7 | PASCAL语言 | 6 | 4 |
3、SC(选课表/成绩表)
Sno | Cno | Grade |
---|---|---|
201215121 | 1 | 92 |
201215121 | 2 | 85 |
201215121 | 3 | 88 |
201215122 | 2 | 90 |
201215122 | 3 | 88 |
一、子查询(subquery)
查询块:在SQL语言中,一个"SELECT-FROM-WHERE"语句称为一个查询块。
嵌套查询:将一个查询块嵌套在另一个查询块的WHERE子句或HAVING短语的条件中的查询称为嵌套查询(nested query)。
例1:查询选择了2号课程的学生姓名(数据表见上文)
SELECT Sname /*外层查询或父查询*/
FROM Student
WHERE Sno IN(SELECT Sno /*内层查询或子查询*/FROM SCWHERE Cno = '2')
例1中,下层查询块被嵌套于上层查询的WHERE条件中,其中下层查询称为内层查询或子查询,上层查询称为外层查询或父查询。
二、不相关子查询(unrelated subqueries)
1.概念
不相关子查询:子查询的查询条件不依赖于父查询,称为不相关子查询。
以上文例1为示例,子查询的查询条件为WHERE Cno = ‘2’,与父查询毫无关联,相互独立,因此为不相关子查询,也称为独立子查询(independent subqueries)。
2.查询逻辑
不相关子查询的查询逻辑为:
step1:先执行子查询,得到选择了2号课程的学生学号集合
step2:再执行父查询,把上一步的学号集合作为条件,从Student表中找出这些学生的姓名
不相关子查询非本文重点,不再赘述
三、相关子查询(related subqueries)
1.概念
相关子查询:顾名思义,子查询的查询条件依赖于父查询,这类子查询称为相关子查询。
2.查询逻辑
相关子查询的查询逻辑为(四步曲):
- step1:先取元组,从父查询中取出一个元组,并将元组的相应属性值给子查询
- step2:执行子查询,按上一步得到的条件执行子查询,并将执行后的结果作为父查询的条件
- step3:执行父查询
- step4:取下一个元组循环上述三步
如果感觉有点绕,没关系,看例子
例2:找出每个学生超过他自己选修课程平均成绩的课程号(数据表见上文)
SELECT Sno, Cno
FROM SC x /*x为表SC的别名,又称为元组变量,表示SC的一个元组*/
WHERE Grade >(SELECT AVG(Grade)FROM SC yWHERE y.Sno = x.Sno)
- step1:先取元组,从父查询SC中取第一个元组x如下,由于子查询中需要x.sno,因此此时y.Sno = ‘201215121’。
Sno | Cno | Grade |
---|---|---|
201215121 | 1 | 92 |
- step2:执行子查询,按y.Sno = '201215121’执行子查询,结果为’201215121’的平均成绩AVG(Grade),即88分(近似值),并将其作为父查询的条件。
- step3:执行父查询,找到’201215121’学生成绩>88的课程,即1号课程,92分。
- step4:从父查询SC中取下一个元组循环上述三步,直到父查询SC中无元组可取,得到查询结果如下:
Sno | Cno | |
---|---|---|
1 | 201215121 | 1 |
2 | 201215122 | 2 |
注意:相关子查询的求解不会一次将子查询求解出来,而是要去遍历父查询的元组来求解子查询。你可以把这个过程理解为一个FOR循环,从父查询表中的第一个元组遍历到最后一个元组
3.带有EXISTS谓词的子查询
EXISTS谓词代码存在量词 ∃
带有EXISTS谓词的子查询不返回任何数据,只产生逻辑真值“true”或逻辑假值“false”。
- 若内层查询结果非空,则外层的WHERE子句返回真值
- 若内层查询结果为空,则外层的WHERE子句返回假值
由EXISTS引出的子查询,其目标列表达式通常都用 * ,因为带EXISTS的子查询只返回真值或假值,给出列名无实际意义。
例3:查询所有选修了1号课程的学生姓名
SELECT Sname
FROM Student
WHERE EXISTS(SELECT *FROM SCWHERE Sno=Student.Sno AND Cno= '1')
我们注意观察代码,子查询WHERE条件中“Sno=Student.Sno”,也就是说子查询的条件依赖父查询,因此这是相关子查询。
回顾并试一试相关子查询“四步曲”:
- step1:先取元组,从父查询Student中取第一个元组如下,由于子查询中需要Student.Sno,因此只用把该元组中的Sno(201215121)给子查询去用就行了。
Sno | Sname | Ssex | Sage | Sdept |
---|---|---|---|---|
201215121 | 李勇 | 男 | 20 | CS |
- step2:执行子查询,按条件(Sno=201215121 AND Cno= ‘1’)执行子查询,发现SC中可以找到201215121学习了课程1,结果非空,父查询的WHERE子句返回真值。
- step3:执行父查询,可以理解为把上述元组的Sname放入结果表中,即“李勇”。
- step4:从父查询Student中取下一个元组循环上述三步,直到父查询Student中无元组可取。
注意:重难点来了,这也是我为什么写这篇文章的原因
NOT EXISTS谓词(重点、难点):
- 若内层查询结果非空,则外层的WHERE子句返回假值
- 若内层查询结果为空,则外层的WHERE子句返回真值
粗看起来,NOT EXISTS谓词就是和EXISTS相反,但它的用处远远大于EXISTS。
先看书上例题:
例4:查询没有选修了1号课程的学生姓名
SELECT Sname
FROM Student
WHERE NOT EXISTS(SELECT *FROM SCWHERE Sno=Student.Sno AND Cno= '1')
例4和例3唯一的区别就是把EXISTS换成了NOT EXISTS:
- 如果一个学生学习了1号课程,内层查询结果非空,外层的WHERE子句返回假值,这个学生不被放入结果表;
- 如果一个学生没学习1号课程,内层查询结果为空,外层的WHERE子句返回真值,这个学生会被放入结果表。
例5:查询选修了全部课程的学生姓名
SELECT Sname
FROM Student
WHERE NOT EXISTS(SELECT *FROM CourseWHERE NOT EXISTS(SELECT *FROM SCWHERE Sno= Student.SnoAND Cno= Course.Cno))
书上没有什么解释直接给了以上这段代码,让人莫名,这也是为什么大家难以理解的原因之一。我这里先不和大家绕存在与不存在,先看另一个问题。
例6:查询学生“201215121”没有选的课程名
SELECT Cname
FROM course
WHERE not exists(SELECT * FROM SC WHERE cno = course.cno and sno = “201215121”)
我们要看一个学生(学生已知为“201215121”)没选什么课,可以拿着Course表里每一门课去SC表里面找,看这位学生到底有没有学。
因此我们可以想到采用相关子查询“遍历”的思想,在父查询中查Course表,遍历Course表中的每一个元组放进子查询SC中。而父、子查询就涉及了这两个表的连接,因此子查询中还需要加入条件cno = course.cno。注:左边的cno其实就是sc.cno。
执行过程如下:
- 取Course表中第一个元组(即课程1),在SC表中找cno = ‘1’ and sno = '201215121’的元组是不是存在,显然存在,因此子查询非空,not exists非空为假,所以1号课程不加入结果表;
- 取Course表中第二个元组(即课程2),在SC表中找cno = ‘2’ and sno = '201215121’的元组是不是存在,显然存在,因此子查询非空,not exists非空为假,所以2号课程不加入结果表;
- 取Course表中第三个元组(即课程3),在SC表中找cno = ‘3’ and sno = '201215121’的元组是不是存在,显然存在,因此子查询非空,not exists非空为假,所以2号课程不加入结果表;
- 取Course表中第四个元组(即课程4),在SC表中找cno = ‘4’ and sno = '201215121’的元组是不是存在,发现不存在,因此子查询为空,not exists非空为真,所以4号课程(操作系统)加入结果表;
- 同理5(数据结构)、6(数据处理)、7(PASCAL)号课程也加入结果表。
再回到例5:查询选修了全部课程的学生姓名
选修了全部课程,等价于“没有课程没有选修”。
而我们在例6中已经知道怎么找某一学生没有选修的课程,所以我们只需要在这个结果上再套用一层not exists,就可以得出没有课程没有选修,并将其拓展至所有学生。
或者这样理解:学生未知,课程未知,我们想知道没有课程没有选修的学生,需要用两层FOR循环(两层not exists),第一层遍历所有学生(用Student表),第二层遍历所有课程(用Course表),再在SC表中,看遍历的值是否存在。
SELECT Sname /*找到一个学生---方式为遍历学生表*/
FROM Student
WHERE NOT EXISTS /*不存在一门课程--方式为遍历课程表*/(SELECT * FROM CourseWHERE NOT EXISTS /*没有选修--方式为遍历选课表来比对(Sno, Cno)*/(SELECT * FROM SCWHERE Sno= Student.SnoAND Cno= Course.Cno))
以执行过程如下:
- 取Student表中第一个元组(201215121),并遍历Course表中每一个元组(1至7),在SC表中找这7种组合可能是不是都存在,显然没有,说明学生“201215121”有课没选修,排除这个学生。
- 取Student表中第二个元组(201215122),并遍历Course表中每一个元组(1至7),在SC表中找这7种组合可能是不是都存在,显然没有,说明学生“201215122”有课没选修,排除这个学生。
- 同理。。。。。
你也可以通过两层FOR循环来理解:
for(在Student表中遍历Sno){for(在Course表中遍历Cno){在SC表中看 Sno= Student.Sno AND Cno= Course.Cno 是否满足如果都满足,说明这个学生选修了所有课如果有一门课不满足,说明这个学生没有选修所有课}
}
尽力了,不懂的话多看一遍?最好是写代码测试一下
提示:这个题还有另一种方法,就是找到选修课程数 = 课程总数的学生,代码如下:
SELECT Sname
FROM Student
WHERE Sno IN (SELECT SnoFROM SCGROUP BY Sno /*以Sno */HAVING COUNT(*)=(SELECT COUNT(*) /*所有课程的门数*/FROM Course))
书上最后一个例题,就留给大家自己去理解了,累了。。。
例7:查询至少选修了学生201215122选修的全部课程的学生号码
即:不存在这样的课程,学生201215122选修了,而某学生没有选
SELECT DISTINCT Sno
FROM SC SCX
WHERE NOT EXISTS(SELECT *FROM SC SCYWHERE SCY.Sno = '201215122' AND NOT EXISTS(SELECT *FROM SC SCZWHERE SCZ.Sno=SCX.Sno AND SCZ.Cno=SCY.Cno))
总结
没什么好总结的,多做几个题,直接就明白了