每次讲到关系代数,总有人问我同一个问题:“这玩意儿除了考试还能干啥?SQL不香吗?”说实话,我当年学《数据库系统概论》时也这么想过,直到后来自己写复杂查询时被嵌套子查询绕晕,回头用关系代数一拆解,才发现原来“翻译成SQL”这件事,本质上就是把关系代数表达式一句句换皮。关系代数不是数据库理论里的老古董,而是你理解查询优化器、写出高效SQL的底层直觉。这篇内容我会把关系代数的所有核心运算拆开揉碎,从集合运算到除法,每个都配上可手算的实例,再带你从自然语言一步步搭出关系代数表达式,最后落到SQL映射和查询优化上,看完你就能把这张“理论地图”装进脑子里。
1. 关系代数到底在解决什么问题
1.1 先把“关系”这个词说清楚
关系代数里的“关系”,指的就是我们在数据库里看到的那个二维表。表的每一行叫元组,每一列叫属性,表名加属性集合就构成了关系模式。比如一张学生表:
Student(Sno, Sname, Ssex, Sage, Sdept)
这行字的意思是:有一张叫Student的表,它有学号、姓名、性别、年龄、所在系这五个属性,这就是关系模式。而表里实际存的每一行,比如('2024001', '张三', '男', 20, 'CS'),就是一个元组。
为什么叫“代数”?因为它是把表当作运算对象,用运算符从已知表推导出新的表。就像小学时用数字做加减乘除,关系代数就是用关系做运算。你输入两个关系,经过某种运算,输出一个新关系。这就意味着,任何关系代数运算的结果依然是“一张表”,依然符合关系的所有性质,因此可以进行嵌套和继续运算——这种封闭性,正是它能作为查询语言理论基础的根源。
1.2 两类运算的划分逻辑
关系代数的运算符可以分为两大类。
第一类是传统集合运算,包括并(∪)、差(−)、交(∩)和笛卡尔积(×)。这类运算来源于数学里的集合论,操作的对象是“关系里的元组集合”,它们要求参与运算的关系具有相容性,也就是说属性个数相同、对应属性的类型一致。想象你把两张表上下叠在一起,列结构一样才能做并、差、交,这个直觉在后面会非常有用。
第二类是专门的关系运算,包括选择(σ)、投影(π)、连接(⋈)和除(÷)。这类运算才是数据库查询真正依赖的“灵魂”,因为它们是针对表的行、列结构专门设计的。选择管行,投影管列,连接管跨表组合,除管“全体满足”这类语义。
这两类运算组合在一起,就能表达几乎所有你想对数据库执行的查询。而且在关系代数里,一个查询就是一个表达式,就像算术里一个计算就是一个算式。这种“表达式”的特质给查询优化带来了可能:同一个查询意图,可以写出多个逻辑等价的关系代数表达式,优化器从中挑一个执行代价最小的。后面我会专门讲这一点。
2. 集合运算:并、差、交、笛卡尔积
2.1 三个小运算:并、差、交
先看并运算。R∪S返回属于R或属于S的所有元组,重复元组只保留一份。注意这里“重复元组会去掉”是关系模型的规定,因为关系是集合,集合里不允许重复元素。实际数据库表一般允许重复行,所以当你要在SQL里表达“并去重”时,用的不是UNION ALL,而是UNION。
并运算的适用前提是R和S相容。举个例子,R是计算机系的学生,S是软件工程系的学生,两个关系的属性都是(Sno, Sname, Ssex, Sage, Sdept),那么R∪S就是这两个系所有学生的并集,去掉了那些同时出现在两个系记录里的重复行(虽然现实里一个人不会同时属于两个系,但集合运算逻辑上要这样处理)。
差运算R−S返回属于R但不属于S的元组。用集合的直觉来理解:从R这张表里,把S里出现过的行全部划掉,剩下的就是结果。差运算最典型的用途是实现“排除”类查询,比如“查计算机系但年龄不大于20岁的学生”,逻辑上就是先选计算机系学生,再排除掉年龄≤20的学生。注意R−S和S−R不是一回事,前者保留R里独有的,后者保留S里独有的,顺序不能反。
交运算R∩S返回同时属于R和S的元组。它可以用差运算表达:R∩S = R − (R − S)。交集用到的场景相对少一些,但它和并、差一样,都是SQL中INTERSECT、EXCEPT对应的理论基础。很多同学在写“查同时选修了A课程和B课程的学生”这类查询时,会本能地想到用WHERE Cno='A' AND Cno='B',这当然是错的,正确的思路就是用交集或者分组聚合。关系代数帮你把这一步逻辑预先想清楚了。
2.2 笛卡尔积:数据库里最容易翻车的运算
笛卡尔积R×S的规则很简单:R的每一行和S的每一行都拼在一起,形成新行。若R有m行、n个属性,S有p行、q个属性,那么R×S有m×p行、n+q个属性。
听着简单,但实际使用中它是性能和语义的双重陷阱。假设学生表有1000行,选课表有5000行,不写连接条件直接做笛卡尔积就是一个500万行的中间结果,很多新手写SQL时忘了JOIN的ON条件,后台瞬间就被这种“隐式笛卡尔积”打爆。关系代数的好处是在表达式层面就把这种风险摊开给你看,凡是出现了×,后面几乎必然跟着一个选择运算来“收窄”它。
笛卡尔积的意义在于它为连接运算提供了“原料”。等值连接本质上就是“先做笛卡尔积,再按指定列相等的条件做选择”。所以你在关系代数里看到R×S别慌,它往往不是一个最终结果,而是组合操作的中间环节。
2.3 算基数、算结果、检验你是否真懂的小题
来一道经典的入门题。设R(A, B)有两行:{(1, a), (2, b)},S(B, C)有两行:{(a, x), (b, y)}。
- R∪S:不行,因为两者属性不同(R是A、B两列,S是B、C两列),不满足相容性,所以不能直接并。
- R×S:得到4行2+2列:{(1,a,a,x), (1,a,b,y), (2,b,a,x), (2,b,b,y)}。
- 如果对R×S做选择σ(R.B=S.B),则得到{(1,a,a,x), (2,b,b,y)},这就是一次等值连接的结果。
这个例子建议你亲手在纸上画一遍,把每一步中间表都写出来。很多同学看公式觉得懂,一让手算就懵,就是因为没有真正“搬运”过数据。关系代数的学习,动手演算比看十遍书都管用。
3. 选择与投影:查询操作的“地基”
3.1 选择:行级别的过滤器
选择的符号是σ,读作sigma,语法是σF(R)。F是选择条件,比如“Sdept='CS'”、“Sage>20”、“Sdept='CS' AND Sage<25”。它的语义非常直观:从R中挑出满足条件F的整行,列结构保持不变。你可以把它理解成Excel里的“筛选”,或者SQL里的WHERE子句。
选择运算有几个性质要特别留意。
第一,选择条件是命题公式,由原子条件和逻辑连接符AND、OR、NOT构成。原子条件形如“属性名 比较运算符 常量/属性名”,比较运算符有=、≠、>、≥、<、≤。写条件时要注意属性名是否存在于R中,否则条件无意义。
第二,选择运算满足串接定律:σF1(σF2(R)) = σF2(σF1(R)) = σF1∧F2(R)。这意味着你想要“计算机系且年龄大于20”的学生,既可以写成σ(Sdept='CS' AND Sage>20)(Student),也可以先选系再选年龄,或者反过来。这个性质在查询优化里很常用——它允许你把选择条件拆开、下推,从而尽早缩小中间结果。
第三,选择结果的元组顺序是不确定的。关系是集合,没有顺序概念。你在写关系代数表达式时不需要关心顺序,但如果你是一个习惯看SQL执行结果的人,可能会觉得“没顺序很难受”。没关系,SQL里的ORDER BY是为展示服务的,和关系代数的语义层无关。
实际做题时,最容易错的地方是条件里用错了属性名,或者把AND写成了OR。还有一个低级但常见的错误:选了行以后还想要原来的所有列,却忘了选择不会改变列集合,它只筛行,根本不需要你指定列。你要是写出σ(Sno, Sname, Sdept='CS')(Student),那说明你把选择当成了“选行+选列”混合体,这个误区一定要纠正。
3.2 投影:列级别的裁剪器
投影的符号是π,语法是πA1,A2,...,Ak(R),A1到Ak是你想保留的属性名。它的作用是从R中选出指定的列,并去掉结果中的重复行。
去掉重复行这五个字,是投影运算最容易被忽略、也最容易在题目里挖坑的地方。假设有一个关系选修SC(Sno, Cno, Grade),你想知道有哪些学生选过课,写了πSno(SC)。如果有一个学生选了三门课,他在SC里有三行但学号相同,那么投影之后这三行在“学号”这一列上完全相同,按照集合的规定,结果里只保留一个。所以πSno(SC)直接给出了所有选过课的学生学号,天然去重。你后续要做计数、做是否存在判断时,这个去重很可能就是你想要的行为。
投影也可以参与嵌套。比如先选择后投影,先筛选符合条件的行,再从这些行里挑列,这是关系代数表达式里最常用的组合模式:πSname(σ(Sdept='CS')(Student))的意思是“先找计算机系学生,再输出他们的名字”。注意顺序不能反:如果你先投影掉Sdept列,后续就没办法用Sdept做选择了。这就是为什么在构造表达式时,你得想清楚每一步之后还剩下哪些列。
3.3 选择+投影组合的经典用法
这类“先选行再选列”的模式,覆盖了数据库日常查询里一大半的需求。我举三个例子:
查询1:查所有年龄小于18岁的学生的姓名和所在系。 π(Sname, Sdept)(σ(Sage<18)(Student))
查询2:查计算机系男学生的学号。 π(Sno)(σ(Sdept='CS' AND Ssex='男')(Student))
查询3:查成绩大于90分的选课记录,只要学号和课号。 π(Sno, Cno)(σ(Grade>90)(SC))
这三个表达式的共同套路是:先σ筛选行,再π裁剪列。你在做关系代数题时,拿到查询需求先想清楚“条件落在哪些属性上,结果要哪些属性”,然后用σ和π把行和列分别收拾干净。
还有一个细节:表达式的顺序会影响中间结果大小。如果一张表有10万行,你要先做投影到2列再做选择,和先做选择再投影,前者的中间结果可能是后者的几十倍。这就是为什么数据库优化器喜欢把选择下推,原则是“先缩行、再缩列”,尽量让每一步中间结果变小。等你以后看执行计划,会经常见到这个思路的具象化。
4. 连接运算与它的亲戚们
4.1 连接的本质是什么
连接运算(⋈)是关系代数里最常用也最需要理解透的运算。它的定义可以拆成两步:第一步,对两个关系做笛卡尔积;第二步,按连接条件从笛卡尔积中筛选出符合条件的元组。所以连接可以表示为:
R ⋈F S = σF(R × S)
F就是连接条件,比如R.Sno = S.Sno、R.Sdept = S.Sdept等。
能从定义这个角度看连接,你就能理解为什么连接操作会带来性能开销——它天然涉及“乘法级”的组合。实际执行时数据库当然不会真的先算一个巨大的笛卡尔积再筛选(那样太慢了),它会用索引、Hash Join、Nested Loop等算法优化。但从语义层面,连接就是“笛卡尔积+选择”,这个等价关系在推导表达式时经常用到。
4.2 等值连接与自然连接:一个关键区别
当连接条件是“属性值相等”时,比如R.A = S.B,就叫等值连接。等值连接结果里会包含两个相连的属性列:R.A和S.B是两列,它们的值在每一行里是相等的,但两张表的属性名可能相同也可能不同,结果都保留。
自然连接是等值连接的一个特殊形式,它要求两个关系在“同名属性”上做等值比较,并且结果中去掉重复的同名列。
来看例子。R(A, B, C)有三行:{(1, a, x), (2, b, y), (3, c, z)},S(B, C, D)有两行:{(a, x, 10), (b, z, 20)}。
R和S的同名属性是B和C。自然连接R⋈S要求R.B = S.B且R.C = S.C:
第一行R(1,a,x):B=a、C=x,正好匹配S中的(a,x,10),所以连成(1,a,x,10)。 第二行R(2,b,y):B=b、C=y,和S中的(b,z,20)不匹配(y≠z),所以丢掉。 第三行R(3,c,z):没有匹配,丢掉。
结果就是{(1, a, x, 10)},只有一行。
如果用等值连接σ(R.B=S.B AND R.C=S.C)(R×S),得到的结果行和自然连接内容一样,但列结构不同——它保留了R.B、R.C、S.B、S.C四个属性列,总共6列。而自然连接会自动合并同名的B和C,只留下(R.A, B, C, D)四列。
很多教材和考试会专门问“自然连接和等值连接的区别”,核心差异就两条:第一,自然连接必须在同名属性上进行等值比较,等值连接不一定同名;第二,自然连接结果去掉重复列,等值连接保留所有列。你做题时只要对比这两点,基本不会错。
4.3 外连接:保留“落单”的行
普通连接只保留能匹配上的行,匹配不上的行直接被舍弃。但有些查询需要保留“落单”的行,这就引出了外连接。
左外连接(R⟕S):结果保留R中所有行,S里没有匹配的属性用NULL填充。 右外连接(R⟖S):保留S中所有行,R里没有匹配的属性用NULL填充。 全外连接(R⟗S):保留两侧所有行。
举一个我经常用来教学的例子。Student表有张三、李四、王五,SC表记录了他们选课的情况,其中王五一门课都没选。那么Student⟕SC的结果里,张三和李四正常显示选课记录,王五那一行Sno、Sname正常,但Cno、Grade是NULL。
这个场景对应SQL就是LEFT JOIN,也是很多人在实际业务里天天用的东西。关系代数里外连接的价值在于,它给了你一种“保住主表全部行”的明确表达方式。考试里外连接一般会结合“查没有选课的学生”来考:先做左外连接,再找SC属性为NULL的行。
另一种连接叫半连接(R⋉S),它在R中返回能与S中某行匹配的那些行,只返回R的列,而且不重复。半连接的用途主要在分布式数据库优化里,用来减少数据传输量。考试大纲不见得要求半连接,但你当扩展了解一下没坏处——它和“IN子查询”语义高度相似。
5. 除法运算:最抽象但最“考题”的运算
5.1 除运算的定义拆解
除法是专门的关系运算里最难理解的一个,很多学生一看到R÷S就懵。我建议你用“披萨切分”的类比来破冰:R÷S的意思是,从R里找出那些“拥有S所要求的所有特征”的元素,每个元素作为一行输出。
形式定义是这样的:设关系R(X, Y)和S(Y),其中Y是两者的共同属性。R÷S的结果是这样一个关系:它包含所有在R中出现、并且其Y部分包含了S中全部Y值的X值。
说得再直白一点:R÷S = πX(R) − πX(πX(R) × S − R)。
这个等价表达式很关键,它说明除法不是一个“凭空出现”的运算,而是可以用投影、笛卡尔积和差来表达的。如果你在考试里忘了除法的计算步骤,也可以用这个公式逐步推。
5.2 用“选修了所有课程”来理解
经典的除法例子永远是学生选课。设有SC(Sno, Cno),Course(Cno),关系代数表达式:
πSno,Cno(SC) ÷ πCno(Course)
这个式子的含义是:找出那些选修了Course里全部课程的学生学号。为什么除法能做到?因为除法的语义就是“R中的X值,必须覆盖S中所有的Y值”。
具体推演一下,假设SC表:
Sno Cno 001 C1 001 C2 002 C1 003 C1 003 C2 003 C3
Course表: Cno C1 C2 C3
那么SC ÷ Course的结果是003,因为003选过C1、C2、C3,覆盖了Course的所有课程;001缺C3,不满足;002只选了C1,更不满足。结果就一行:003。
这个例子会出现在几乎所有数据库教材的习题里,因为“全部”这个量化词(全称量词)在关系代数里没有直接的运算符,而除法恰好把“对所有的Y,X都满足”这个语义装进去了。
5.3 除运算如何用其他运算表达,以及它的使用边界
用公式表达:R÷S = πX(R) − πX(πX(R) × S − R)。
我来解释一下这个公式在做什么。第一步πX(R)得到所有出现过的X值;第二步πX(R) × S是把所有这些X值都配上S的全部Y值,形成一个“理想化的完整组合表”;第三步减去R,找出那些“应该在但实际不在R里的组合”;第四步再投影出X,得到的就是“缺失了某些Y的X”;最后用全体X减去这些缺失的X,剩下的就是“Y全部覆盖的X”。
按这个公式也可以手算出上面例子的答案,你可以自己走一遍。
使用边界上要注意:做除法时,S的所有属性必须都包含在R里,否则没法比较。而且除法只对“全称量词”类查询有效。像“查选修了全部课程且成绩都及格的学生”,除法的基本思路一样,只是要先把SC里成绩不及格的记录剔除掉再做除法。除法和“双重否定”(NOT EXISTS嵌套)在SQL里是等价的,这个映射关系在后续查询练习里非常有用。
6. 从查询需求到关系代数表达式:完整实战
6.1 先建立一张“翻译对照表”
拿到自然语言查询需求,怎么一步步写成关系代数表达式?很多初学者卡住的原因是不知道从哪里下手。我的建议是先做一个思维转换:把每个查询需求拆成“我要筛什么行、取什么列、需要拼哪几张表”三个问题。
下面是常见查询短语和运算的一一对应,建议你贴在桌前:
| 查询需求 | 对应运算 |
|---|---|
| 条件是某列的取值 | σ选择 |
| 结果只要某些列 | π投影 |
| 需要多张表的数据 | 连接⋈或笛卡尔积× |
| 去掉重复行 | 投影自带去重 |
| 满足任一条件 | OR |
| 同时满足多个条件 | AND |
| 属于A但不属于B | 差− |
| “全部”或“所有” | 除法÷ |
| 保留主表所有行 | 外连接⟕/⟖/⟗ |
6.2 四个经典查询,边做边讲
继续用学生选课数据库:Student(Sno, Sname, Ssex, Sage, Sdept),Course(Cno, Cname, Cpno, Ccredit),SC(Sno, Cno, Grade)。
查询1:查询“张三”选修的所有课程号。 思路:先找出张三的学号,再到SC里找记录。两步走: πCno(σ(Sname='张三')(Student ⋈ SC)) 这里用连接把Student和SC拼起来,然后按姓名筛选,最后取课程号。如果先把Student选到只剩张三,再和SC连接,理论上结果一样。 πCno(σ(Sname='张三')(Student) ⋈ SC) 这个写法其实更优化,因为连接前已经把Student缩小到一行了。这个“先缩行再连接”的思想,在后面讲优化器时也是核心。
查询2:查询选修了“数据库”课程且成绩在90分以上的学生姓名。 πSname(σ(Cname='数据库' AND Grade>90)(Student ⋈ SC ⋈ Course)) 三步连接后一起过滤,表达了“学生、选课、课程”三张表的关联关系。运用连接和选择,一行表达式搞定。
查询3:查询没有选修任何课程的学生姓名。 πSname(Student ⟕ SC) − ?不对,这里有个更直观的解法。先找出所有选过课的学生学号,再用学生表减去这些学号,最后投影姓名: πSname(Student ⋈ (πSno(Student) − πSno(SC))) 这个表达式的思路是:所有学生减去选过课的,等于没选课的学生。用差运算表达“排除”。
查询4:查询选修了全部课程的学生姓名。 πSname(Student ⋈ (πSno,Cno(SC) ÷ πCno(Course))) 除法先找到选全所有课程的学号,再连接学生表取姓名。这个表达式把除法的威力完全展现出来了。
6.3 一个多层嵌套的复杂查询,完整走一遍
查询:查询选修了“数据库”和“操作系统”两门课程的学生学号。
很多同学一看到“和”,第一反应是AND。但这里有个坑:一行SC记录只能是一门课的选课记录,不可能同时出现Cname='数据库' AND Cname='操作系统'。正确做法是先分别找到两门课的选课记录,再取学号交集。
设C1 = πSno(σ(Cname='数据库')(SC ⋈ Course)),C2 = πSno(σ(Cname='操作系统')(SC ⋈ Course)),最终答案是C1 ∩ C2。
当然也可以换一种思路:按学号分组,统计该生选修这两门课的数量等于2: πSno(σ(Cname='数据库' OR Cname='操作系统')(SC ⋈ Course)) 这个要先选出两门课的所有记录,再按学号分组、计数。但关系代数标准里没有专门的GROUP BY聚合运算,所以用交集更贴合纯关系代数。
这里想强调一个经验:做关系代数题时,遇到“同时满足多个条件但条件存在于不同行”的情况,优先考虑连接之后的交集或除法,而不是AND。AND只能处理同一行里多个属性同时满足的情况。
7. 从关系代数到SQL与查询优化
7.1 关系代数表达式如何“翻译”成SQL
关系代数是SQL的“底层逻辑”,SQL是关系代数的“实现方言”。知道它们之间的映射关系,会让你学SQL时有一种“原来如此”的顿悟感。
| 关系代数 | SQL |
|---|---|
| σF(R) | SELECT * FROM R WHERE F |
| πA,B(R) | SELECT DISTINCT A, B FROM R |
| R ∪ S | SELECT ... FROM R UNION SELECT ... FROM S |
| R − S | SELECT ... FROM R EXCEPT SELECT ... FROM S |
| R ∩ S | SELECT ... FROM R INTERSECT SELECT ... FROM S |
| R × S | SELECT * FROM R CROSS JOIN S |
| R ⋈F S | SELECT * FROM R JOIN S ON F |
| R ⟕ S | SELECT * FROM R LEFT JOIN S ON F |
| R÷S | 用NOT EXISTS双重嵌套子查询 |
举一个映射实例:查询“选修了全部课程的学生姓名”,SQL可以写成:
SELECT Sname FROM Student WHERE NOT EXISTS ( SELECT 1 FROM Course WHERE NOT EXISTS ( SELECT 1 FROM SC WHERE SC.Sno = Student.Sno AND SC.Cno = Course.Cno ) );外层“NOT EXISTS Course里不存在该生没选的课”,就是除法的SQL形态。
7.2 查询优化器靠什么“等价变换”
前面反复提到“同一个查询可以有多个关系代数表达式”,比如:
πSname(σ(Sdept='CS')(Student ⋈ SC)) 和 πSname(σ(Sdept='CS')(Student) ⋈ SC)
逻辑上完全等价,但执行代价差别很大:第一个表达式先把Student和SC做全量连接,再筛计算机系;第二个表达式先把Student缩小成计算机系学生,再和SC连接。显然第二个中间结果小得多。
数据库查询优化器干的事,就是在语法分析之后生成多个候选执行计划,估算代价,选一个最便宜的。而优化器的很多转换规则,根源就是关系代数的等价变换定律。
优化器最常用的启发式规则,用关系代数的话讲就是:
- 选择下推:把σ尽量往表达式树下方移动,让选择尽早执行,尽早缩小关系。
- 投影下推:把π尽量往下移动,减少参与后续连接的列。
- 连接顺序重排:用交换律和结合律调整连接顺序。
- 合并选择串接:把多个选择合成一个,减少扫描次数。
这些规则不是考试唯一的目标,但在真实的数据库性能调优里,它们决定了你是写出了一个慢查询还是一个快查询。
7.3 为什么我们要学关系代数,三个实际理由
第一,它帮你写出逻辑正确的查询。很多“SQL写不出来”的情况,是你没有先分析清楚查询的逻辑结构。先在纸上写一个关系代数表达式,再翻译成SQL,能大大降低出错率。
第二,它帮你理解执行计划。以后你打开数据库的执行计划,看到Filter、Nested Loop、Hash Join、Projection这些名词,本质上都是关系代数运算符的物理实现版本。懂了σ、π、⋈,执行计划就不再是火星文。
第三,它帮你做SQL调优。知道选择下推能缩小中间结果,你就明白为什么“先过滤再连接”比“先连接再过滤”快;知道笛卡尔积的代价是指数级增长,你就不会再写出没有连接条件的JOIN。
8. 学习关系代数的常见误区与应试建议
8.1 四个高频翻车点
误区一:把选择和投影混在一起。选择只筛行,不筛列;投影只筛列,不筛行。考试里最容易出的错误是在σ后面跟一堆属性列表,比如σ(Sno, Sdept='CS')(Student),这是不合法的。记住:σ括号里只能有逻辑条件表达式,不能有属性列表。
误区二:忽略投影的去重语义。题目问你“选修了课程的学生有哪些”,如果你的表达式是πSno(SC),没问题,因为投影会自动去重。但如果你以为结果会保留每个选课记录而不去重,那你就违背了“关系是集合”的底线。在SQL里,SELECT DISTINCT Sno FROM SC才对应这个关系代数表达式。
误区三:自然连接与等值连接混淆。自然连接要求同名属性、等值比较、去掉重复列三件事同时发生;等值连接只要条件成立就行,结果列保留双方属性。考试如果考“R和S自然连接后有几列”,记得把同名属性合并。
误区四:除运算的方向搞反。R÷S里,S是“除数”,是你要求所有X都必须覆盖的特征集合。Course表只有Cno一列,是用来做除法的“除数”,不是被除数。做题前先标清楚哪个是X、哪个是Y。
8.2 一个可执行的练习路线
我教过很多学生,总结出一条比较稳的学习路径:
第一步,把并、差、交、笛卡尔积四个运算自己造表手算一遍,直到能不看书写出结果。 第二步,用σ和π做二十道简单查询题,每道题都写出表达式并口算验证。 第三步,重点攻克连接和除,把本章的例子自己完整推演三遍。 第四步,挑十道历年考研/期末考试真题,要求自己先写关系代数表达式,再写SQL,两边对照。 第五步,等到学查询优化时,回来重新看一遍关系代数,你会发现以前记不住的等价变换定律一下子都顺了。
练习时强烈建议你准备一张大草稿纸,把每个表达式的执行过程画成一棵表达式树。比如R⋈S就可以画成“R和S作为叶子节点,连接作为根节点”,选择、投影就是树上的中间节点。画树不是浪费时间,它就是在训练你用“数据流动”的视角理解查询的执行过程。
8.3 从“会做题”到“会思考”
考试层面,关系代数的题大多是给自然语言查询,写表达式;或者给表达式,算结果。这两个方向你都要练。前者练的是“把语义结构翻译成运算结构”,后者练的是“把运算结构还原成数据结果”。
到了真实工作场景,你可能很少手写关系代数,但你在写SQL、看执行计划、调索引的时候,用的全是关系代数的直觉。那些能一眼看出两条SQL是否等价的人,往往不是在背语法,而是在脑内画了一棵关系代数表达式树。
所以我的建议是:别把关系代数当一门纯理论课去背,把它当成“SQL思维的草稿纸”。它最大的价值,是在你面对复杂查询时,提供一个不受具体语法干扰的逻辑坐标系。你先想清楚用什么运算、按什么顺序组合,再落成SQL,整个查询的设计就变得非常清晰。