SQL语句基操(五)——依旧是查询
本篇文章记录了第八次作业
使用的数据库是SQL Server,使用的数据库管理软件是SQL Server Management Studio.
上一篇文章:SQL语句基本操作练习(四)
上次说到嵌套查询,剩了个EXISTS留在最后面。这是因为这一块涉及到离散数学中的谓词逻辑,有点绕,我希望能够放在这篇文章中仔细地再来讲讲。
(一)嵌套查询
4.带有EXISTS谓词的子查询
EXISTS这个英文单词的意思是“存在”,在这里代表存在量词∃。在SQL中我们可以使用EXISTS和NOT EXISTS来进行逻辑判断。用EXISTS谓词的子查询返回的是true和false,而不是数据。内层查询为非空返回true,内层查询为空返回false,NOT EXISTS谓词与之相反。由EXISTS引出的内层查询的目标列表达式,我们一般都使用*,因为这里给出列名是没有实际意义的。
[例 3.60] 查询所有选修了1号课程的学生姓名
SELECT Sname
FROM Student
WHERE EXISTS
(SELECT *
FROM SC
WHERE Sno=Student.Sno AND Cno= '1');
很明显这是一个相关子查询,Student的值传入了子查询内。这里的结构逻辑是先从Student表中取出一个值,Sno,与SC表中的Sno值进行比对,若是二者相等且Cno为1,则会像父查询返回true表示存在这样一组数据,然后将其对应的Sname存入关系表中。如此反复直至Sno中所有值都被检验过。
在这里我觉得EXISTS主要起了一个逻辑判断的作用,填补了之前运算符表达式的一种表达上的空缺。这一题也有别的查询语句可以实现,比如运用连接查询很简单就可以查到。
[例 3.61] 查询没有选修1号课程的学生姓名。
厘清上一问后,这一题就显得很明了了,只是将EXISTS换成了NOT EXISTS。下面给出语句:
SELECT Sname
FROM Student
WHERE NOT EXISTS
(SELECT *
FROM SC
WHERE Sno=Student.Sno AND Cno= '1');
注意:带有EXISTS和NOT EXISTS谓词的子查询可能不能被其他形式的子查询等价替换,但所有带有IN谓词、比较运算符、ANY和ALL谓词的子查询都可以用EXISTS谓词子查询来替换。也就是说EXISTS是它们的上位替代,但效率并不一定更高,这还是要看具体的使用情况。
像之前的例3.55,对还是那个找同系学生的题,就可以用EXISTS来替换IN。
SELECT Sno,Sname,Sdept //例3.55改
FROM Student S1
WHERE EXISTS
(SELECT *
FROM Student S2
WHERE S2.Sdept = S1.Sdept AND //S1中找出和S2刘晨dept相同的
S2.Sname = '刘晨');
//可以看到相比于原来的语句变得复杂了,所以这里其实没必要替换
[例 3.62] 查询选修了全部课程的学生姓名
选修全部课程,这里的全部可以翻译成∀,但是SQL中是没有全称量词∀的。不过我们有存在量词。我们可以利用存在量词来婉转表达全称量词的含义。我们知道:
(
∀
x
)
P
≡
¬
(
∃
x
(
¬
P
)
)
(\forall x)P \equiv \lnot (\exists x(\lnot P))
(∀x)P≡¬(∃x(¬P))
这是个什么意思呢?看右边式子里面有两个否定,其实就是将全程量词变成了存在量词,双重否定表肯定嘛。那么带入到具体的语境中,比如这里的命题:
- 选修了全部课程的学生
就可以转化为
- 没有一门课程不选修的学生
好活!这样一来我们就能使用EXISTS来写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,先记着。先看最内层的语句可以看到这里先比对的是Sno,也就是学号。当学号比对成功后会继续比对Cno,也即是课程号。
假设第一个比对的学号是001,选修了所有课,一共有3门课。那么在Course的查询中,每比对一个Cno都会向Course查询中返回一个false(使用的是NOT EXISTS),那么3门课就是3个false,即
F
∨
F
∨
F
=
F
F\lor F \lor F = F
F∨F∨F=F
意思就是返回给最外层的是一个F值,然后再把它NOT EXISTS一下就成了T值,即该学生选修了全部的课程。如果该学生有一门课没选修,上式中就会出现一个T值,最后的值肯定就是T了,那么在最外层就会变为F,意为该生没能选修全部课程。
若是我们不使用NOT EXISTS而是EXISTS呢?那么这就和我们的命题不符了,只能说是存在而不能说是全部,这样最后的结果就是只要是选了一门课的都会跳出来。
[例 3.63] 查询至少选修了学生201215122选修的全部课程的学生号码
对于这个查询,我们用逻辑蕴含来表达:查询学号为x的学生,对所有的课程y,只要201215122学生选修了课程y,则x也选修了y。
-
用P表示谓词 “学生201215122选修了课程y
-
用q表示谓词 “学生x选修了课程y
则上述查询为: ( ∀ y ) p → q (\forall y)p \to q (∀y)p→q
又 p → q ≡ ¬ p ∨ q p \to q \equiv \lnot p \lor q p→q≡¬p∨q
有 ( ∀ y ) p → q ≡ ¬ ( ∃ y ( ¬ ( p → q ) ) ) ≡ ¬ ( ∃ y ( ¬ ( ¬ p ∨ q ) ) ) ≡ ¬ ∃ y ( p ∧ ¬ q ) (\forall y)p \to q \equiv \lnot (\exists y(\lnot (p \to q)))\equiv \lnot (\exists y(\lnot(\lnot p \lor q))) \equiv \lnot \exists y(p \land \lnot q) (∀y)p→q≡¬(∃y(¬(p→q)))≡¬(∃y(¬(¬p∨q)))≡¬∃y(p∧¬q)
转化为语义是指:不存在这样的课程y,学生201215122选修了y,而学生x没有选。
SELECT DISTINCT Sno
FROM SC SCX
WHERE NOT EXISTS
(SELECT *
FROM SC SCY
WHERE SCY.Sno = '201215122' AND
NOT EXISTS
(SELECT *
FROM SC SCZ
WHERE SCZ.Sno=SCX.Sno
AND SCZ.Cno=SCY.Cno));
因为这里用了三次SC表,我们给它们取SCX、SCY、SCZ三个别名。很明显这又是相关子查询,思路大体上和上一题一样,但更为复杂。SCX层是要进行查询比对的层,它传递了一个Sno进入到了最内层,SCY层作用是将课程号传入最内层。在SCZ这一层中,拿着上一层得到的课程号,与选到的课进行比对,若是选过这节课则返回true到上一层,否则返回false。
与上题一样,我们假设被查询的学生把所有课都选过了一次,201215122选了3节课。那么该生在最内层都会返回T,到了第二层则会变成F,那么在第二层中要SELECT出符合条件的因为都是F,所以返回最外层的肯定是T(NOT EXISTS),最外层的T指的是没有201215122选的课该学生没有选
若是该生选了201215122的课还选了别的课,而且有一些课没选,那么在第二层那些没选的课201215122肯定也没选,一个T(没选的课到第二层变成T)一个F(201215122没选)结果仍是F(这里是并关系,和上一题的第二层是不一样的),与刚刚的最终情况相同。
若是该生选的课比201215122的课少,那么第二层会变成T,最后反应出来就是F。
花了一些口舌说明了这个SQL语句是正确的,但是真要我写一个还是很难哈。
(二)集合查询
集合查询主要有3种,并操作UNION,交操作INTERSECT和差操作EXCEPT。集合操作的查询结果是元组的集合,显示上较为直观。
注意:参加集合操作的各查询结果的列数必须相同,对应项的数据类型必须相同 。
[例 3.64] 查询计算机科学系的学生及年龄不大于19岁的学生。
这是一个并集的操作,写出查询CS系和年龄不大于19的语句加上一个UNION就好了,没啥别的。
SELECT *
FROM Student
WHERE Sdept= 'CS'
UNION /*若要保留重复元组请使用UNION ALL*/
SELECT *
FROM Student
WHERE Sage<=19;
[例 3.65] 查询选修了课程1或者选修了课程2的学生。
看到或者这个词就意味着我们可以选择并操作来完成这次查询。查询课程1再查询课程2,二者一并即可。
SELECT Sno
FROM SC
WHERE Cno='1'
UNION
SELECT Sno
FROM SC
WHERE Cno= '2';
[例3.66] 查询计算机科学系的学生与年龄不大于19岁的学生的交集。
题目既然已经点出来这是交集,那么直接上INTERSECT即可。
SELECT *
FROM Student
WHERE Sdept='CS'
INTERSECT
SELECT *
FROM Student
WHERE Sage<=19
/*又或者可以使用单表查询*/
SELECT *
FROM Student
WHERE Sdept='CS' AND Sage<=19;
[例 3.67] 查询既选修了课程1又选修了课程2的学生
既···又···这个关联词表示这一题同样是个交集的操作。
SELECT Sno
FROM SC
WHERE Cno='1'
INTERSECT
SELECT Sno
FROM SC
WHERE Cno='2';
/*亦或者选择嵌套查询*/
SELECT Sno
FROM SC
WHERE Cno='1' AND Sno IN
(SELECT Sno
FROM SC
WHERE Cno='2');
[例 3.68] 查询计算机科学系的学生与年龄不大于19岁的学生的差集。
题目点出来了这是一个差集,我们不需要多思考。这题求差集的意思就是要查询CS系中年龄大于19岁的学生。
SELECT *
FROM Student
WHERE Sdept='CS'
EXCEPT
SELECT *
FROM Student
WHERE Sage <=19;
/*Another Version*/
SELECT *
FROM Student
WHERE Sdept='CS' AND Sage>19;
(三)基于派生表的查询
子查询不仅可以出现在WHERE子句中,
还可以出现在FROM子句中,这时子查询生成的临时派生表成为主查询的查询对象。
如例3.57和例3.60可以改写为以下形式:
/*例3.57 找出每个学生超过他自己选修课程平均成绩的课程号*/
SELECT Sno, Cno
FROM SC, (SELECT Sno, Avg(Grade) FROM SC GROUP BY Sno) /*此处将所有的平均成绩按Sno排列,被下面作为临时派生表的基础*/
AS Avg_sc(avg_sno,avg_grade) /*建立一个临时派生表Avg_sc,里面有avg_sno,avg_grade两个属性*/
WHERE SC.Sno = Avg_sc.avg_sno and SC.Grade >=Avg_sc.avg_grade
/*例3.60 查询所有选修了1号课程的学生姓名*/
SELECT Sname
FROM Student,
(SELECT Sno FROM SC WHERE Cno=' 1 ') AS SC1
/*通过FROM子句生成派生表,AS关键字可以省略掉,但是你必须为派生关系指定别名*/
WHERE Student.Sno=SC1.Sno;
临时派生表究竟是什么意思呢?看上面的例3.57重制版,第二行建立了一个储存平均成绩并排序的表,第三行就是将其声明为临时派生表,让它可以拿出来被使用。第四行就是一个普通的连接查询了。总的来说临时派生表就是将一组处理过的数据作为临时的表,使查询能够更方便地进行。
结语
在学习查询语句的过程中,我感受最深刻的就是一个查询目的不一定只有一种语句对应。也就是说,很多语句都可以实现某一个操作,具体的选择则需要用户根据自己的习惯以及效率来确定。不同的人写出的语句风格不一样但最后的目的是相同的。
再一个就是感受到了数学思想在计算机科学中的应用。尽管是SQL语句这种看起来和编程没有什么关系的应用,它也无时无刻不体现着数学思想:关系代数、逻辑谓词和交并差集等等。
下一篇文章:SQL语句基本操作练习(六)
参考文献:
[1]萨师煊,王珊,数据库系统概论.5版.北京:高等教育出版社,2014.
[2]David老师的PPT.