☰
《数据库系统概论》第三章课后题:关系代数与SQL转换深度解析
2026/10/14 16:01:28 网站建设 项目流程

一直觉得《数据库系统概论》第三章是全书最容易被低估的一章。表面上是学SQL语法,实际上它强行让初学者用关系代数的眼光重新看一遍“数据操作”这件事。尤其是课后题第4、第5题,几乎每次考试都有人栽在这两道题上:关系代数写不完整,或者把关系代数翻译成SQL时漏条件、乱嵌套。这篇文章就把这两道题拆开细讲,目标是让你做完之后,能彻底搞懂题目背后的套路。

我先把题目背景还原一下。教材第4题、第5题通常围绕一个非常经典的选课模型展开:学生表S、课程表C、选课表SC。表结构大概是这样的:

  • S(学号Sno,姓名Sname,性别Ssex,年龄Sage,所在系Sdept)
  • C(课程号Cno,课程名Cname,先修课号Cpno,学分Credit)
  • SC(学号Sno,课程号Cno,成绩Grade)

第4题一般是要求“用关系代数表达式表示下列查询”,第5题则把其中一部分查询改成SQL语句,并要求比较不同写法。看起来是两道题,实际上是一条线:先逼你用集合思维描述查询,再让你把集合思维落成SQL。这篇文章不只是给答案,更想把整个思考链路讲明白。

1. 先把题目看明白:第三章到底在考什么

1.1 这题不是让你背语法,是让你建立三层映射

很多同学做这道题,第一反应是去翻SQL语法,把SELECT、FROM、WHERE背得滚瓜烂熟,结果看到关系代数表达式还是懵。原因很简单:关系代数和SQL是两种不同的思维语言。

关系代数更接近数学里的集合运算,它的核心动作只有几个:选择(筛选行)、投影(挑选列)、连接(把两张表拼起来)、集合运算(交、并、差)、除法(“全都满足”的判断)。而SQL是在关系代数之上加了工程化语法,比如GROUP BY、HAVING、ORDER BY、聚合函数、子查询、视图等。

做第4题和第5题,本质是在练三件事:

  1. 把中文查询需求翻译成关系代数操作树;
  2. 把关系代数表达式翻译成SQL语句;
  3. 反向理解:SQL在数据库内部会被优化器拆解成关系代数执行计划。

这个能力在面试和实际开发中非常有用。很多数据库调优问题,说到底就是在关系代数层面判断“该把选择条件下推,还是先做连接”。

1.2 一个可以复现的题目设定

由于不同印次的教材课后题排版略有差异,我这里按最常见的版本展开。第4题通常包含这么几个小问:

  1. 查询年龄小于20岁的男学生的学号和姓名;
  2. 查询选修了课程号为“2”且成绩大于等于90分的学生的学号;
  3. 查询至少选修了课程“1”和课程“2”的学生学号;
  4. 查询选修了全部课程的学生的姓名和所在系。

第5题一般要求:将第4题中的第3、第4小问转换为SQL语句,并分析两种写法的异同。

我后面所有的讲解都基于这个设定。如果你手上的教材数字对不上,也不影响,解题方法和易错点完全通用。

2. 第4题:关系代数表达式的拆解套路

2.1 先把四类基本操作刻进脑子

关系代数一共就那么几个运算符,如果你记不住,我给你一个特别接地气的类比:把每张表想成一个Excel工作簿。

  • 选择σ,就是Excel里的“筛选行”,给定一个条件,只留下符合条件的记录;
  • 投影π,就是“挑选列”,只保留需要的字段;
  • 连接⋈,就是VLOOKUP,按共同的字段把两张表横向拼起来;
  • 集合运算,就是Excel里对两列数据做交集、并集、差集;
  • 除法÷,稍微抽象一点,你可以理解成“找出那些覆盖了全集范围的对象”。

做第4题时,我强烈建议你按“先定表,再定条件,最后定输出列”的顺序来写。比如第1小问,“年龄小于20岁的男学生”,一看就是只涉及S表,不需要连SC和C。直接在S表上先做选择,再做投影:

π(Sno, Sname)(σ(Sage<20 ∧ Ssex=‘男’)(S))

这里要注意两点。第一,选择条件里的比较符号要写清楚,小于20是sage<20,不是<=。第二,关系代数的结果是一个集合,默认没有重复行,所以不需要像SQL那样刻意写DISTINCT。这个差异后面第5题会再次出现。

2.2 连接操作的执行顺序有多重要

第2小问“查询选修了课程号为2且成绩大于等于90分的学生的学号”,涉及SC表,但不需要S表,因为只查学号。写起来就是:

π(Sno)(σ(Cno=‘2’ ∧ Grade>=90)(SC))

如果题目改成“查学生的姓名”,才需要SC和S做连接。连接的本质是把SC里满足条件的那几条记录,去S表里把对应的姓名带出来。写成:

π(Sname)(σ(Cno=‘2’ ∧ Grade>=90)(SC ⋈ S))

大多数新手在这个地方会犯一个隐蔽错误:先做连接,再筛选,虽然结果一致,但中间过程会生成一个巨大的中间表。考试时不算错,但如果放在真实数据库优化里,这就是典型的“未提前应用选择条件下推”。我建议从现在开始就养成习惯:能先缩小的关系,就先做选择和投影,再做连接。这个习惯在学查询优化时会帮你省很多力气。

2.3 除法:几乎所有人的噩梦

第4小问“查询选修了全部课程的学生的姓名和所在系”是这道题里最值钱的一问。它考的是除法运算。

先想清楚逻辑:一个学生选修了全部课程,意味着“这个学生选过的课程集合”包含“全部课程集合”。放到集合论里,这正是除法的定义。

标准写法是这样:

π(Sno, Sname, Sdept)(S ⋈ (π(Sno, Cno)(SC) ÷ π(Cno)(C)))

我之前自己学的时候,怎么都理解不了除法,后来换了个角度才通:除法是在找“被除数里那些在除数覆盖范围内、且每个除数元素都至少匹配一次”的商。拿这个题目来说,被除数是“每个学生选了什么课”的明细集合,除数是“全部课程编号”,商就是那些“课程编号覆盖率达到100%”的学生学号。

如果考试时不允许写“÷”,也可以用等价的方式表达,即“不存在一门课程,这个学生没选过”。这句话翻译成带NOT EXISTS的双重否定,逻辑上完全等价。但我还是建议优先掌握除法符号,因为阅卷时老师第一眼希望看到的就是这个。

2.4 括号和属性名的规范不能乱

关系代数表达式特别容易因为括号位置出错。我见过不少同学写长表达式,括号从一开始就少了一个,到结尾整个式子结构坍塌。我自己的经验是:完成表达式后,从最内层括号开始,一层一层向外读一遍,用自然语言复述,看能不能对上题目要求。如果能对上,基本就是对的;对不上,优先检查连接条件和投影字段。

另外,两个关系里出现同名字段时,要在字段名前面加关系名限定,比如SC.Cno。如果不加,阅卷时会被认为概念不清晰,而且你自己也容易分不清是S里的Cno还是SC里的Cno。第4题整个就是一个“先小范围试错、再正式做题”的典型。

3. 第5题:从关系代数到SQL,中间隔着一层执行计划

3.1 关系代数的“集合思维”与SQL的“包思维”差异

第5题让你把关系代数翻译成SQL,这里有个很多人意识不到的点:关系代数处理的是集合,默认没有重复;SQL处理的是多重集合,也就是“包”,默认允许重复行。

举个例子,第4题第3小问“查询至少选修了课程1和课程2的学生学号”,关系代数写成交集非常简洁:

π(Sno)(σ(Cno=‘1’)(SC)) ∩ π(Sno)(σ(Cno=‘2’)(SC))

但SQL里没有直接的交集操作符吗?其实是有的,就是INTERSECT。但考试如果只允许用基本的SELECT-FROM-WHERE,就需要换一种写法。最常见的两种:

写法一:用IN子查询嵌套。

SELECT Sno FROM SC WHERE Cno = '1' AND Sno IN ( SELECT Sno FROM SC WHERE Cno = '2' );

写法二:用分组加HAVING。

SELECT Sno FROM SC WHERE Cno IN ('1', '2') GROUP BY Sno HAVING COUNT(DISTINCT Cno) = 2;

这两种写法各有各的坑。写法一如果子查询结果集很大,可能性能不好;写法二的核心是COUNT(DISTINCT Cno)=2,这里的DISTINCT千万不能省,否则如果存在重复选课记录,计数会虚高,导致错误结果。我建议考试时优先写第一种,因为它语义最直白,不容易被扣分。

3.2 分组过滤里最隐蔽的坑:HAVING和WHERE的区别

讲解写法二时,必须说一说WHERE和HAVING的边界问题。WHERE是在分组之前筛选行,HAVING是在分组之后筛选组。哪怕你把条件写对了,如果不知道这个区别,后面做“查询平均成绩大于90分的学生”这类题时一定会乱。

我再扩展一下:查询每个学生选课的平均成绩,但只显示平均成绩大于等于85分的学生。SQL是:

SELECT Sno, AVG(Grade) AS AvgGrade FROM SC GROUP BY Sno HAVING AVG(Grade) >= 85;

这个查询里,WHERE不存在,因为条件是作用在组上。如果你想知道“只看成绩及格的选课记录,再算平均”,就需要先WHERE过滤再分组:

SELECT Sno, AVG(Grade) AS AvgGrade FROM SC WHERE Grade >= 60 GROUP BY Sno HAVING AVG(Grade) >= 85;

这两个SQL从表面上看只差一个WHERE,但对结果的影响可以非常大。我读书时在这个点上栽过跟头,所以强烈建议你每次写GROUP BY前,先在草稿上写出“先淘汰哪批行”和“再淘汰哪批组”这两步。

3.3 第4小问转SQL:为什么优先用NOT EXISTS

现在说第5题里最难的转换:把“查询选修了全部课程的学生的姓名和所在系”翻译成SQL。

最自然、最保险的写法是双重否定:

SELECT Sname, Sdept FROM S WHERE NOT EXISTS ( SELECT 1 FROM C WHERE NOT EXISTS ( SELECT 1 FROM SC WHERE SC.Sno = S.Sno AND SC.Cno = C.Cno ) );

先说清楚它的逻辑:外层表S里取一个学生,进入第一个NOT EXISTS,我们要证明“这个学生没有不选的课”。于是再进一层,遍历课程表C,如果发现某门课这个学生没在SC里匹配上,内层就查出记录,说明存在“没选的课”,外层NOT EXISTS判断为假,这个学生被淘汰。反过来,如果所有课程都能匹配上,内层始终查不到记录,外层NOT EXISTS成立,学生被保留。

有人会问:能不能用NOT IN?理论上可以,但实际工作中非常容易踩到NULL的雷。因为NOT IN遇到子查询结果里有NULL,整个判断会变成“不确定”,最终返回结果集往往为空。这不是理论上“可能”,而是我实测过很多次,一旦课程表C里存在Cno为NULL的行,NOT IN版本就直接废了。NOT EXISTS是基于行匹配的,不受NULL影响,所以在这道题里,我强烈建议你养成NOT EXISTS的习惯。

另外,这种相关子查询在数据库里是“外层循环套内层循环”的语义,专业点叫嵌套循环连接。数据量大时性能不一定最好,但考试和作业里,正确性第一位。

3.4 一个能验证两种写法等价的办法

第5题有时候还会要求你“讨论两种写法的等价性”。我教你一个非常朴素的办法:写一段包含少量数据的测试脚本,把自己写的SQL放进数据库里跑,看两个查询结果是否完全一致。

我自己当年用的测试数据是这样的:三张表各放4-5行,故意构造一个“选了全部课程的学生”、一个“只选了一门课的学生”、一个“一门课都没选的学生”,跑完对照结果。只要你能解释清楚为什么某个数据行该出现、某个数据行不该出现,等价性问题基本就稳了。

4. 参考答案与自查清单:做完题怎么判断对错

4.1 一份可直接对照的示例答案

按照前面设定的题目,我给出一个可以对照的答案版本。注意,这只是其中一种正确写法,不代表唯一解。

第4题:

(1) π(Sno, Sname)(σ(Sage<20 ∧ Ssex='男')(S)) (2) π(Sno)(σ(Cno='2' ∧ Grade>=90)(SC)) (3) π(Sno)(σ(Cno='1')(SC)) ∩ π(Sno)(σ(Cno='2')(SC)) (4) π(Sname, Sdept)(S ⋈ (π(Sno, Cno)(SC) ÷ π(Cno)(C)))

第5题:

-- 第3小问:至少选修课程1和课程2的学生学号 SELECT Sno FROM SC WHERE Cno = '1' AND Sno IN ( SELECT Sno FROM SC WHERE Cno = '2' ); -- 第4小问:选修了全部课程的学生的姓名和所在系 SELECT Sname, Sdept FROM S WHERE NOT EXISTS ( SELECT 1 FROM C WHERE NOT EXISTS ( SELECT 1 FROM SC WHERE SC.Sno = S.Sno AND SC.Cno = C.Cno ) );

我给的答案有一个特点:优先保证可读性。关系代数第4问用除法,SQL用NOT EXISTS,都是阅卷时最容易认可的思路。

4.2 验证结果的两类核心测试数据

给出答案之后,我强烈建议你用两类数据验证。第一类是正常数据,包含一个三科全选的学生,比如S001;一个选了两科但缺第三科的学生S002;一个一科都没选的学生S003。跑SQL,看结果里是否只有S001的姓名。第二类是边界数据,把SC表里S001的某条选课记录复制成两条一模一样的,验证COUNT(DISTINCT Cno)的写法是否能正确处理重复选课。这两种数据覆盖了90%的隐藏错误。

4.3 一个可以打印出来的自查清单

做题时对照下面这张表过一遍,能省下大量查错时间。

检查维度具体要检查什么常见失误
投影字段SELECT后面的列是否真的满足题目要求多选列导致结果不对齐
筛选条件WHERE里的比较符、数值是否抄对漏掉成绩下限、学号写成课程号
连接条件多表连接时是否把所有关联键写全两张表只写一个连接条件
分组语义GROUP BY和HAVING是否放对位置把HAVING过滤条件放到WHERE
NULL兼容子查询里是否存在NULL风险NOT IN遇上NULL导致结果为空
去重语义关系代数默认去重,SQL是否需要DISTINCT分组计数时漏写DISTINCT
嵌套相关EXISTS内外层关联条件是否写对内层忘记用外层别名关联

这张表看起来简单,但我每次做这类题都会拿它从头到尾过一遍。越是觉得“这题我会”的时候,越容易在一个小地方翻车。

5. 作业与考场的避坑清单:这题最常见的扣分点

5.1 关系代数与SQL混写导致的阅卷失控

我批过课程作业,也见过很多考试答案,最常见的扣分原因不是不会,而是混写。比如在关系代数表达式里突然冒出一个GROUP BY,或者在SQL查询里出现σ、π这种符号。阅卷老师的心理预期非常明确:第4题用关系代数符号,第5题用SQL关键字。两者混在一起,哪怕逻辑是对的,也会被认为“概念不清”,丢分很冤。

5.2 连接条件漏写,导致笛卡尔积爆炸

还有一种特别经典的错误:多个表连接时,只写了筛选条件,没写连接条件。比如查学生姓名和选课成绩,如果写成:

SELECT S.Sname, SC.Grade FROM S, SC WHERE SC.Grade >= 90;

这个查询会先把S和SC做笛卡尔积,再筛选成绩大于90的行。结果大概率是对的,但中间过程和语义都乱了。如果后续再加一张课程表,笛卡尔积会膨胀得更厉害,结果离谱程度指数上升。我建议你从现在开始养成显式写JOIN的习惯,哪怕是老式逗号连接,也要在脑子里明确“连接条件完整了吗”这个问题。

5.3 题干关键词与运算的对应关系要背熟

这类题目非常讲究“关键词触发”。我整理过一张小表,考试前反复看几遍很有帮助:

题干说法首选关系代数操作首选SQL写法
至少选修了X和Y交集IN子查询 / INTERSECT
选修了全部课程除法双重NOT EXISTS
没有选修任何课程差集NOT EXISTS / NOT IN(注意NULL)
只选修了X差集加条件NOT EXISTS + 课程条件
平均/最大/最小/计数聚合操作GROUP BY + 聚合函数

别小看这个对应关系,实际上它才是这两道题真正的考点。题目换再多张皮,核心永远是这几个语义。

5.4 时间分配建议

考试里遇到这两道题,我建议先花2分钟读题并圈出关键词,再用5-8分钟写关系代数,最后用5分钟转换SQL。如果某个子查询一时想不明白,先跳过,把能写的都写上,避免在一个空上死磕导致后面时间不够。我当年考试就吃过亏:卡在第4小问的除法上,结果后面简单题没时间写。这个教训现在想起来还是肉疼。

6. 从课后题到真实数据库:这门功夫为什么值钱

6.1 关系代数是查询优化器的“思考语言”

你可能会想:这些关系代数表达式,工作以后用得着吗?说句实话,大部分日常CRUD开发用不上,但只要你开始跟慢查询、执行计划打交道,关系代数就无处不在了。

数据库在执行SQL前,会把SQL解析成一棵关系代数操作树,然后进行等价变换。比如“先过滤再连接”和“先连接再过滤”,结果可能一样,但代价可能差出几个数量级。优化器做的就是把操作树重新排列到一个更优的执行顺序。你能看懂执行计划里的Filter、Join、Aggregate这些算子,靠的还是当初做第4题、第5题时练出来的直觉。

6.2 复杂SQL写不出来时,先画关系代数草图

做数据分析的同学,经常要写那种带三四层子查询的长SQL。我的个人习惯是:先在草稿纸上把关系代数操作链画出来,标清楚哪一步做选择、哪一步做投影、哪一步做连接,然后再转成SQL。这样做有三个好处:结构清晰、不容易漏条件、排错时能一眼定位到问题环节。

我接触过不少同事,遇到复杂查询直接上手抠SQL,结果改了一下午也没写对。我说你先别急着写,把问题拆成关系代数步骤再说,十分钟就理顺了。这不是玄学,是第三章这两道题真正想教给你的思路。

6.3 一点过来人的体会

按我个人经验,这两道题花一个晚上彻底想通透,比刷十道同类练习题更有用。因为大部分练习题只是换数字,而这两道题逼着你把集合思维、连接语义、子查询、分组过滤、NULL边界全部过了一遍。如果你能不看答案,独立把第4题和第5题完整写出来,并且能解释清楚每一步为什么这么写,那你的数据库地基就比绝大多数人扎实了。以后不管是面试时聊SQL优化,还是工作中排查慢查询,你都会感谢当年认真抠过这两道题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询