《数据库系统》SQL语言之复杂查询 子查询(NOT)IN子查询 θ some/θ all子查询 (NOT) EXISTS子查询(理论理解分析+实例练习)
利用SQL语言表达复杂查询
为什么需要子查询?
通常需要进行如下条件的判断:
- 集合成员资格:某一元素是否是某一个集合的成员 (NOT)IN子查询
- 集合之间的比较:某一个集合是否包含另一个集合等 θ some/θ all子查询
- 集合基数的测试:测试集合是否为空;测试集合是否存在重复元组 (NOT) EXISTS子查询
子查询:出现在where子句中的select语句被称为子查询(subquery),子查询返回了一个集合,可以通过与这个集合的比较来确定另一个查询集合。
三种类型的子查询:
(NOT)IN - 子查询;θ-some/θ-all 子查询;(NOT) ESISTS 子查询
相关子查询与非相关子查询
非相关子查询
带有子查询的select语句区分为内层和外层。
非相关子查询:内层查询独立进行,没有设计任何外层查询相关信息的子查询。
前面所讲述的实例均为非相关子查询。
相关子查询
内层查询需要依靠外层查询的某些参量作为限定条件才能进行的子查询。
外层向内层传递的参量需要使用外层的表名或表别名来限定。
本文中出现的表的属性结构参照如下:
Course(Cnumber, Tnumber)
Student(Snumber,Sname)
SC(Snumber,Cnumber,Score)
Teacher(Tnumber,Tname)
EG: 求学过001号课程的同学的姓名
select Sname
from Student Stud
where Snumber in (Select Snumber from SC where Snumber = Stud.Snumber -- 内层Stud依靠外层参量and Cnumber = '001');
注意:相关子查询只能由外层向内层传递参数,而不能反之;这也称为变量的作用域原则。
(NOT) IN 子查询
基本语法
表达式 [not] in (子查询)
语法中,表达式的最简单形式就是列名或常数。
语义:判断某一表达式的值是否在子查询的结果中。
EG: 列出张三、王五同学的所有信息
select * from Student
where Sname in ('张三','王五');
此处直接使用了某一子查询的结果集合(枚举形式)。
如果该集合是一致的固定的,可以如上直接书写。
相当于
select * from Student
where Sname ='张三' or Sname = '王五';
EG: 列出选修了001号课程的学生的学号和姓名
select Snumber, Sname from Student
where Snumber in (select Snumber from SC where Cnumber = '001');
EG: 求即学过001号课程,又学过002号课程的学生的学号
select Snumber from SC -- 在选课表SC中找学号
where Cnumber = '001' and -- 课程号为001号Snumber in (select Snumber from SC where Cnumber = '002'); -- 且学号在(学了002号课程的学号)集合中
EG: 列出没学过李明老师讲授课程的所有同学的姓名
“从全体学生中,排除掉那些学过李明老师课程的学生,剩下的就是我们要找的学生。”
select Sname from Student
where Snumber not in (select Snumber from SC,Course C,Teacher Twhere T.Tname = '李明' and SC.Cnumber = C.Cnumber and C.Tnumber = T.Tnumber);
- 外层主查询
select Sname from Student where Snumber not in (...)
从Student表中选择学生姓名Sname。 条件是:学生的学号Snumber 不在 某个集合 中。
这个“集合”就是内层子查询的结果。
- 内层子查询 - 找出“学过李明课”的学生
(select Snumber from SC, Course C, Teacher T where T.Tname = '李明' and SC.Cnumber = C.Cnumber and C.Tnumber = T.Tnumber)
这个子查询的目的是:找出所有至少学过一门“李明”老师所授课程的学生的学号。
连接逻辑:
from SC, Course C, Teacher T: 这是一个三表连接。
where T.Tname = ‘李明’:首先在Teacher表中定位姓名为“李明”的老师记录。
and C.Tnumber = T.Tnumber:然后将Course表与Teacher表连接,找出“李明”老师讲授的所有课程(C.Cnumber)。
and SC.Cnumber =C.Cnumber: 最后将SC选课表与上一步的结果连接,找出所有选修了这些课程的学生记录。
select Snumber:从最终连接结果中,选出这些学生的学号。
内层子查询的最终结果是一个学号集合,这个集合包含了所有学过“李明”老师课程的学生。
θ some/θ all 子查询
基本语法:
表达式 θ some (子查询)
表达式 θ all (子查询)
语法中,θ是比较运算符:<, >, >=, <=, =, <>
语义:将表达式的值与子查询的结果进行比较:
- 如果表达式的值至少与子查询结果的某一个值相比较满足θ关系,则“表达式 θ some (子查询)”的结果便为真。
- 如果表达式的值与子查询结果的所有值相比较都满足θ关系,则“表达式 θ all (子查询)”的结果便为真。
EG: 找出工资最低的教师姓名
select Tname from Teacher
where Salary < all (select Salary from teacher);
-- ()中的子查询为所有教师的工资的集合
EG: 找出001号课成绩不是最高的所有学生的学号
select Snumber from SC
where Cnumber = '001' and Score < some (select Score from Course where Cnumber = '001'); -- 只要有一个成绩比他高,则一定不是最高
EG: 找出所有课程都不及格的学生姓名(相关子查询)
select Sname from Student
where 60 > all (select Score from SC where Snumber = Student.Snumber); -- (子查询)表示所有课程成绩的集合-- 60 > all 表示集合中的所有成绩均小于60
EG: 找出001号课成绩最高的所有学生的学号
select Snumber from SC
where Cnumber = '001' and Score >= all (select Score from SC where Cnumber = '001');
EG: 找出98030101号同学成绩最低的课程号(非相关子查询)
select Cnumber from SC
where Snumber = '98030101' and Score <= all (select Score from SC where Snumber = '98030101');
EG: 找出张三同学成绩最低的课程号(相关子查询)
select Cnumber from SC, Student S -- 涉及到姓名,需要表Student
where Sname = '张三' and SC.Snumber = S.Snumber -- 张三的所有选课信息表and Score <= all (select Score from SC where Snumber = S.Snumber); -- S.Snumber 为外层参数
- 主查询与表连接
select Cnumber from SC, Student S where Sname = '张三' and SC.Snumber = S.Snumber
from SC, Student S: 连接SC表和Student表。
where Sname = ‘张三’: 在Student表中定位姓名为"张三"的学生。
and SC.Snumber = S.Snumber: 将SC表与Student表连接,找出张三的所有选课记录。
select Cnumber: 最终要选择的是课程号。
到这一步,我们得到了张三同学的所有选课记录(包括课程号和对应的成绩)。
- 相关子查询 - 核心逻辑
and Score <= all (select Score from SC where Snumber = S.Snumber)
这是查询的核心部分,我们来详细分解:
子查询部分(select Score from SC where Snumber = S.Snumber)
这是一个相关子查询,因为它的条件 Snumber = S.Snumber 中的 S.Snumber 来自外层查询。
对于外层查询中的每一行(即张三的每一门课),这个子查询都会执行一次。
子查询的作用:每次执行时,它都会找出张三的所有课程成绩(一个成绩集合)。
<= ALL 运算符 <= ALL 的意思是"小于等于集合中的每一个元素"。
对于外层查询正在检查的某门课的成绩Score,如果它小于等于张三所有课程成绩中的每一个成绩,那么这门课就是成绩最低的课程(可能有多个课程并列最低分)。
等价变换关系
需要注意:如下两种表达方式含义是相同的
等价:
表达式 = some (子查询)
表达式 in (子查询)
如下两种表达方式含义却是不同的
不等价:
表达式 not in (子查询)
表达式 <> some (子查询)
与 not in 等价的是
等价:
表达式 not in (子查询)
表达式 <> all (子查询)
(NOT) EXISTS 子查询
基本语法:
[not] exists (子查询)
语义:子查询结果中有无元组存在。
EG: 检索选修了赵三老师主讲课程的所有同学的姓名
select DISTINCT Sname from Student
where exists (select * from SC, Course, Teacher where SC.Cnumber = Course.Cnumber and SC.Snumber = Student.Snumber -- 对应所有选课信息and Course.Tnumber = Teacher.Tnumber and Tname = '赵三'); --且选课的课程老师为赵三
不加not形式的exists谓词可以不用,比如上述例子可以直接写成:
select DISTINCT Sname from Student, SC, Course, Teacher
where SC.Snumber = Student.Snumber and SC.Cnumber = Course.Cnumberand Course.Tnumber = Teacher.Tnumber and Tname = '赵三');
然而not exists却可以实现很多新功能。
EG: 检索学过001号教师主讲的所有课程的所有同学的姓名
select Sname from Student
where not exists -- 不存在 not exists(select * from Course -- 有一门001教师主讲课程where Course.Tnumber = '001' and not exists -- 该学生没学过(select * from SCwhere Snumber = Student.Snumber and Cnumber = Course.Cnumber));
意为:不存在有一门001号教师主讲的课程该同学没学过
EG: 列出没学过李明老师讲授任何一门课程的所有同学的姓名
select Sname from Student
where not exists (select * from Course, SC, Teacherwhere Tname ='李明' and Course.Tnumber = Teacher.Tnumberand Course.Cnumber = SC.Cnumber and Student.Snumber = SC.Snumber);
列出至少学过98030101号同学学过的所有课程的同学的学号
对于结果中的任何一个学生A,不存在这样一门课程:这门课‘98030101’学了,但学生A没学。
“关系除法”操作,在SQL中通常使用双重NOT EXISTS嵌套查询.
select DISTINCT Snumber from SC SC1
where not exists (select * from SC SC2where SC2.Snumber = '98030101' and not exists (select * from SC where Cnumber = SC2.Cnumber and Snumber = SC1.Snumber);
- 最外层查询
select DISTINCT Snumber from SC SC1 ...
SC SC1: 为成绩表SC起了一个别名SC1。这个SC1代表的是候选学生的选课记录。
查询会遍历SC1中的每一条记录(即每一个学生的每一门课),但最终我们只关心不同的学号Snumber,所以用了DISTINCT。
WHERE后面的条件将决定哪个候选学生的学号有资格被选出来。
- 中层子查询 - 核心逻辑
(select * from SC SC2where SC2.Snumber = '98030101' and not exists ...)
这是整个查询的灵魂。我们逐层来理解:
SC SC2: 同样为成绩表SC起了另一个别名SC2。这个SC2代表的是目标学生 ‘98030101’ 的选课记录。
where SC2.Snumber = ‘98030101’: 从SC2中筛选出只属于‘98030101’同学的选课记录。
and not exists (…):这是关键中的关键。它的意思是:对于‘98030101’学过的一门课(由SC2.Cnumber表示),不存在于某个地方。
现在,我们把最内层子查询加进来,看看“不存在于什么地方”。
- 最内层子查询 - 连接候选学生与目标课程
(select * from SC where Cnumber = SC2.Cnumber and Snumber = SC1.Snumber)
这个查询在整个SC表中(没有用别名)查找一条记录。
查找的条件是:
Cnumber = SC2.Cnumber: 课程号等于目标学生‘98030101’当前正在检查的那门课(来自SC2)。
Snumber = SC1.Snumber: 并且学号等于外层候选学生当前的学号(来自SC1)。
这个查询的实质是:检查候选学生SC1.Snumber是否也选修了目标学生‘98030101’正在检查的这门课SC2.Cnumber。
EG: 已知SPJ(Sno, Pno, Jno, Qty),其中Sno供应商号,Pno零件号,Jno工程号,Qty数量,列出至少用了供应商S1供应的全部零件的工程号
对于结果中的任何一个工程A,不存在这样一种零件:这个零件供应商S1供应,但工程A没使用。
select DISTINCT Jno from SPJ SPJ1
where not exists (select * From SPJ SPJ2 where SPJ2.Sno = 'S1' and not exists (select * from SPJ SPJ3 where SPJ3.Pno = SPJ2.Pno and SPJ3.Jno = SPJ1.Jno));
该查询的结构和逻辑与前一个例子完全相同。
- 最外层查询
select DISTINCT Jno from SPJ SPJ1 ...
SPJ SPJ1: 为供应情况表SPJ起了一个别名SPJ1。这个SPJ1代表的是候选工程的零件使用记录。
查询会遍历SPJ1中的记录,但最终我们只关心不同的工程号Jno,所以用了DISTINCT。
WHERE后面的条件将决定哪个候选工程的工程号有资格被选出来。
- 中层子查询 - 核心逻辑
where not exists (select * From SPJ SPJ2 where SPJ2.Sno = 'S1' and not exists ...)
这是整个查询的灵魂。
SPJ SPJ2: 为SPJ表起了另一个别名SPJ2。这个SPJ2代表的是目标供应商 ‘S1’ 的零件供应记录。
where SPJ2.Sno = ‘S1’: 从SPJ2中筛选出只属于供应商‘S1’的供应记录(即S1供应了哪些零件)。
and not exists (…): 这是关键。它的意思是:对于‘S1’供应的一个零件(由SPJ2.Pno表示),不存在于某个地方。
现在,我们把最内层子查询加进来,看看“不存在于什么地方”。
- 最内层子查询 - 连接候选工程与目标零件
(select * from SPJ SPJ3 where SPJ3.Pno = SPJ2.Pno and SPJ3.Jno = SPJ1.Jno)
这个查询在SPJ表中查找,并起了别名SPJ3。
查找的条件是:
SPJ3.Pno = SPJ2.Pno: 零件号等于目标供应商‘S1’当前正在检查的那个零件(来自SPJ2)。
SPJ3.Jno = SPJ1.Jno: 并且工程号等于外层候选工程当前的工程号(来自SPJ1)。
这个查询的实质是:检查候选工程SPJ1.Jno是否也使用了目标供应商‘S1’正在检查的这个零件SPJ2.Pno。
本文以哈尔滨工业大学战德臣老师讲授《数据库系统》为系统依据整理得出。具体可前往链接【哈工大】数据库系统 战德臣(全23讲)-哔哩哔哩