要理解除操作,我们首先要引入“象集”这个概念。
其实象集很简单,就跟我们学过的函数对应关系差不多,只不过函数是“一对一”或者“多对一”,而象集恰好相反,是“一对多”。
引入一个例子看看。假设我们有一张学生选课表SC(sno, cno, grade),其中sno的学生的学号,cno是课程号,grade是分数。这张表记录了学生选修某门课的成绩。
sno | cno | grade |
---|---|---|
2020001 | 001 | 93 |
2020001 | 002 | 95 |
2020001 | 003 | 90 |
2020002 | 001 | 98 |
2020002 | 003 | 87 |
2020003 | 002 | 75 |
(备注:一共只有三门选修课:“001”,“002”,“003”)
我们不给出“象集”的任何定义,先直接求出象集,求完之后,你也就明白了,注意前面说的一对多的关系。
2020001的象集{(001,93),(002,95), (003,90)}
2020002的象集{(001,98),(003,987)}
2020003的象集{(002,75)}
有了象集的概念后,我们现在来理解“除运算”。假设我们有两个关系R(X,Y)和关系S(Y,Z),那么
R÷S = { tr[X] | tr∈R ∧ ∏Y(S)⊆Yx } 其中Yx为x在R中的象集。
是不是懵逼了?什么鬼东西?
其实啊,这都是纸老虎,没那么难!我们只需要从列(属性)和行(元组)来理解就可以了。
列(属性):R ÷ S得到的新的关系(表),它的列是从R中的列中去掉R和S相交的列。
行(元组):R中每一个x对应的象集应该包含S的投影
这就是“除运算”,这么说可能还是有些抽象,我们继续用上面给出的学生选课例子来研究,为了方便我们暂时先将SC的grade属性去掉,然后我们再加一个课程关系C(cno,cname)
SC:
sno | cno |
---|---|
2020001 | 001 |
2020001 | 002 |
2020001 | 003 |
2020002 | 001 |
2020002 | 003 |
2020003 | 002 |
C:
cno | cname |
---|---|
001 | 操作系统 |
002 | 计算机网络 |
003 | 数据结构 |
我们通过这两个表求SC÷C,我们从列和行两个步骤来进行求解。
第一步(列):SC和C公共的列是cno,所以R÷S必然只剩下R中的sno列,故列先被确定下来了
SC÷C
sno |
---|
2020001 |
2020001 |
2020001 |
2020002 |
2020002 |
2020003 |
第二步(行):
C的投影为(001,002,003),而SC中sno的象集中包含(001,002,003)的只有2020001,故,行也被确定下来了
SC÷C
sno |
---|
2020001 |
聪明的你可能已经发现,SC÷C表示的含义是“查询选修了全部课程的学生”,其实除操作非常适合用于求“至少使用了…的全部”之类的查询
我们刚刚讨论了除操作的关系代数,那么怎么用SQL语句来表示呢?事实上,sql语言并没有定义除操作,甚至连全称量词都没有,但是有存在量词EXISTS 和 NOT EXISTS,所以通常的思路就是将全程量词转变为存在量词来实现除操作。这种也是大多数教材普遍的讲法,但是我们今天不用这种方法,因为那种方法有些绕,使得我们很容易看懂别人写的,但是让自己写却写不出来。所以我们用另一种非常好理解的方式:从除操作的定义出发来解决,不过这要求我们知道什么是右外连接,不用慌,活着就是为了折腾,我都为你准备好了。
什么是“外连接”呢?这恐怕得从“连接(严格来讲应该是自然连接)”说起!我们还是使用上面的SC表和C表,注意表的变化!
SC:
sno | cno |
---|---|
2020001 | 001 |
2020001 | 002 |
2020001 | 003 |
C:
cno | cname |
---|---|
001 | 操作系统 |
002 | 计算机网络 |
003 | 数据结构 |
004 | 计算机组成原理 |
当我们对两张表做连接的时候,我们会将SC和C中cno相等的元组拼接起来,于是得到新的关系
SC∞C:
sno | cno | cname |
---|---|---|
2020001 | 001 | 操作系统 |
2020001 | 002 | 计算机网络 |
2020001 | 003 | 数据结构 |
细心的你一定发现了,计算机组成原理这么课因为没人选,所以连接运算时就丢失了,这就是所谓的“悬浮元组”。为了让悬浮元组也出现,于是人们就折腾出了外连接、左外连接、右外连接,我们这里直接说右外连接吧。
当C中的元组的cno与SC中的元组的cno没有对应关系的时候,我们不丢弃这个选组,仍然将其显示出来,用NULL填充没有匹配的字段,也就是下面这样:
SC∝C:
sno | cno | cname |
---|---|---|
2020001 | 001 | 操作系统 |
2020001 | 002 | 计算机网络 |
2020001 | 003 | 数据结构 |
NULL | 004 | 计算机组成原理 |
了解了右外连接后我们就可以利用它这个性质来做除运算了。我们再次使用上面的例子(注意SC和C的变化)
SC:
sno | cno | grade |
---|---|---|
2020001 | 001 | 93 |
2020001 | 002 | 95 |
2020001 | 003 | 90 |
2020002 | 001 | 98 |
2020002 | 003 | 87 |
2020003 | 002 | 75 |
C:
cno | cname |
---|---|
001 | 操作系统 |
002 | 计算机网络 |
003 | 数据结构 |
现在我们求选修了全部课程的学生的学号
我们先写关系代数:
① 全部课程的课程号:∏cno(C)
②除:SC ÷ ∏cno(C)
③投影取学号:∏sno(SC ÷ ∏cno(C))
关系代数很容易写出,但是SQL语句稍微有些复杂,现在我们就用外连接来实现。
SQL语句
SELECT DISTINCT sno
FROM SC a
WHERE NOT EXISTS(
SELECT * FROM
(SELECT sno, cno FROM SC b WHERE a.sno = b.sno) x
RIGHT OUTER JOIN
(SELECT cno FROM C) y
ON x.cno = y.cno
WHERE x.sno IS NULL
);
莫慌,我来解释一下这条SQL语句。拿数据说话,注意我调整了一下
SC:
sno | cno | grade |
---|---|---|
2020002 | 002 | 75 |
2020001 | 001 | 93 |
2020001 | 002 | 95 |
2020001 | 003 | 90 |
C:
cno | cname |
---|---|
001 | 操作系统 |
002 | 计算机网络 |
003 | 数据结构 |
外层循环先取出一条记录 SELECT DISTINCT sno FROM SC a
:(2020002, 002, 75)
我们拿到这个学生的学号2020002,然后找出他选修的所有课程:SELECT sno, cno FROM SC b WHERE a.sno = b.sno
:只有一门课002
然后用002与所有课程做外连接,得到如下的结果:
sno | cno |
---|---|
NULL | 001 |
2020002 | 002 |
NULL | 003 |
显然,有两个sno为空了,所以NOT EXISTS不通过,2020002不是选修了全部课程的学生
以同样的方式对2020001操作得到下面的结果
sno | cno |
---|---|
2020001 | 001 |
2020001 | 002 |
2020001 | 003 |
sno没有NULL,所以2020001是选修了全部课程的学生,将其学号输出。