关系代数入门:六大基本运算与SQL查询的底层逻辑
关系代数这东西我在刚学数据库系统概论的时候也懵了很久。看着满屏的σ、π、⋈这些符号第一反应是“这不是数学课的内容吗怎么跑到数据库里来了”。但等你真正做题、写SQL、甚至后面看查询优化器执行计划的时候才会意识到一个扎心的事实所有关系型数据库的查询逻辑底层都是关系代数在撑着。SQL只是它的一层“皮”而关系代数才是真正的“骨”。这篇文章我不打算照着教材念定义而是想从一个过来人的角度把关系代数的整套体系拆开揉碎它到底解决什么问题、六个基本运算各自是什么逻辑、为什么这么说、怎么把它翻译成你天天写的SQL、以及做题时最容易踩的坑。不管你是在准备数据库期末考试、考研复试还是单纯想把SQL的理解往上提一个档次这篇文章都适合你慢慢看。1. 关系代数到底在解决什么问题1.1 数据库系统里为什么要“代数化”先说个很直觉的问题你想从数据库里拿数据最少需要哪几种操作无非就是三件事——看哪些表、筛哪些行、取哪些列。再加一些组合比如把两张表拼起来、把两次查询结果合并。SQL里你写一条SELECT语句感觉是在“描述需求”但在数据库底层你的SQL会被解析成一棵操作树这棵树上的每个节点几乎都能对应到一个关系代数运算。关系代数本质上是一套以集合为对象的运算体系。集合里的元素是“元组”你可以理解成一行数据整个表就是一个集合。那么对表的操作就可以被抽象成对集合的运算并、差、笛卡尔积这些集合论里原本就有的操作再加上专门为“关系模型”设计的操作选择、投影、连接、除。学关系代数最大的价值是让你看懂一个查询“为什么是这样”。比如SQL里连接为什么有时候慢因为笛卡尔积是O(n×m)级别的操作。为什么WHERE条件能优化因为选择运算满足交换律可以先筛选再连接数据量小了连接自然就快。这些如果不建立在关系代数的基础上你对SQL的理解就停留在“背语法”的层面。1.2 六个基本运算与“一脉相承”的扩展运算教材上通常说关系代数有六个基本运算选择σ、投影π、并∪、差-、笛卡尔积×、重命名ρ。为什么是这六个因为它们是“元操作”其他所有运算连接、交、除等都可以由它们推导出来。连接⋈可以先用笛卡尔积再配合条件做选择交集∩可以用差运算表示后面细讲除运算÷则是结合了笛卡尔积和差运算的一种复杂组合。这种“用最小集推导一切”的设计思想正是关系代数的优雅之处。它强调了系统的完备性和最小性而你真正需要掌握的连接、交、除都是建立在基本运算之上的“语法糖”。学习的时候先把六种基本运算吃透后面的扩展运算其实都是顺势推导。1.3 关系代数在课程体系和实际应用中的位置在数据库系统概论第六版这类教材里关系代数一般出现在关系模型那一章承上启下上面承接“关系模型的基本概念域、元组、关系”下面开启“关系数据库的标准语言SQL”。你在学校可能更关注的是考试会不会考但实际上关系代数在业界也远没被淘汰——SQL查询优化器内部使用的就是关系代数等价变换规则比如谓词下推、投影下推这些优化手段本质上是把关系代数表达式做得更“经济”。所以你学关系代数等于在做两件事一是理解SQL的底层语义二是为将来学习查询优化打底子。理解了这层意义你就会明白花时间学这堆符号是值得的。2. 六大基本运算逐个拆解定义、逻辑、SQL对照2.1 选择σ给表“横向切一刀”选择运算的符号是希腊字母σ读作sigma作用是从一个关系中筛选出满足给定条件的元组。用大白话说就是按行过滤把符合条件的行留下不合条件的行扔掉。形式化写法是σ_条件(关系名)比如有一个学生表StudentSnoSnameSageSdept2023001张伟20CS2023002李娜19IS2023003王强21MA想找计算机系CS的学生写成σ_SdeptCS(Student)结果就是张伟那一行。如果想找年龄大于20的σ_Sage20(Student)王强就中选了。条件里支持比较运算符、≠、、、≥、≤和逻辑运算符且∧、或∨、非┐。比如“计算机系并且年龄小于21”σ_SdeptCS ∧ Sage21(Student)SQL对照就是SELECT * FROM Student WHERE SdeptCS AND Sage21;这里有个初学者很容易忽略的点选择运算不会改变关系模式也就是说列数不变只是行的子集。而且选择操作满足交换律σ_条件1(σ_条件2(R)) σ_条件2(σ_条件1(R))所以理论上先筛哪个条件都行实际优化时会把更“紧”的条件先执行。2.2 投影π给表“纵向切一刀”投影用希腊字母π读作pi作用是挑选关系中的某些列并去掉重复行。如果说选择是横切那投影就是竖切。写法π_列名1, 列名2(关系名)比如只看所有学生的学号和姓名π_Sno, Sname(Student)结果就是去掉Sage和Sdept两列的两行数据。这里有个关键特性投影会自动去重。比如你投影出所有院系列如果CS系有10个学生最终只会显示一个“CS”。这跟SQL里的SELECT DISTINCT是一个语义。SQL对照SELECT DISTINCT Sdept FROM Student;要注意的是投影虽然去掉了列但结果仍然是合法的关系集合不允许重复元组所以去重是必须的。这一点和SQL中默认不去重的SELECT不一样——SQL的SELECT默认保留所有行相当于只“投影”但不去重。这一点在做题和写SQL对照时会经常产生混乱后面我会单独说。2.3 并∪两个集合“叠起来”并运算要求两个关系模式相同属性数量一致、对应属性的域一致然后把两个关系的元组合并成一个关系同时自动去重。写法R ∪ S比如有两个表选修了数据库的学生的学号集合A选修了操作系统的学生的学号集合B。想知道“至少选修了其中一门课”的学生的学号就是π_Sno(SC1) ∪ π_Sno(SC2)SQL对照SELECT Sno FROM SC WHERE CnoDB UNION SELECT Sno FROM SC WHERE CnoOS;注意SQL里的UNION默认也去重如果想保留重复行要用UNION ALL而关系代数里并运算的结果是集合本身就无重复。2.4 差-实现“拥有A但不拥有B”差运算是在R中有但S中没有的元组组成的集合。同样要求两个关系模式相容。写法R - S这一步非常关键因为**“差”是表达否定查询的核心工具**。比如“选了数据库但没选操作系统的学生”π_Sno(选DB) - π_Sno(选OS)SQL里没有直接的“差”关键字MySQL连EXCEPT都不支持通常用NOT IN实现SELECT Sno FROM SC WHERE CnoDB AND Sno NOT IN (SELECT Sno FROM SC WHERE CnoOS);这里我多说一句差运算与并运算合在一起可以推导出交运算。定义是R ∩ S R - (R - S)这个公式很有用它说明“既在R又在S”等于“从R中去掉那些在R但不在S里的行”。有些教材会把交作为基本运算之一但从理论最小集的角度交可以由差来定义。2.5 笛卡尔积×两张表“硬拼”笛卡尔积可以说是理解连接运算的钥匙。它把关系R中的每个元组都与关系S中的每个元组组合成一行。如果R有m行n列S有k行l列那么R×S就有m×k行nl列。写法R × S举个例子如果Student有两个元组Course有两门课它们的笛卡尔积就是2×24行每一行都是“一个学生一门课”的组合哪怕这个学生根本没选这门课。SQL对照SELECT * FROM Student CROSS JOIN Course;笛卡尔积在查询中很少直接使用否则数据量爆炸但它是连接的基础。你在SQL里写FROM A JOIN B ON条件本质上就是在笛卡尔积上做选择运算只是数据库优化器会想办法先做条件过滤避免真的生成完整的笛卡尔积。2.6 重命名ρ给关系或属性改个名重命名运算的符号是ρ读作rho作用是把关系或属性改名主要用于两类场景处理自连接表和自身比较和消解属性名冲突。写法ρ_S(原关系名) // 给关系起别名S ρ_新属性名←旧属性名(R) // 给属性改名比如你想在同一个选课表SC中找出“选了课程1又选了课程2的学生学号”就要把SC和自己做笛卡尔积这时必须给其中一个副本改名为SC1才能区分两边的属性π_SC1.Sno(σ_SC1.SnoSC2.Sno ∧ SC1.Cno1 ∧ SC2.Cno2(ρ_SC1(SC) × ρ_SC2(SC)))SQL对照就是表别名SELECT a.Sno FROM SC a, SC b WHERE a.Snob.Sno AND a.Cno1 AND b.Cno2;这个操作在写关系代数表达式和SQL时都非常常见可以说没有重命名自连接根本无法表达。3. 扩展运算连接、交、除终结你的理解盲区3.1 连接运算笛卡尔积的“升级版”连接运算可以理解为在笛卡尔积上做选择。但根据连接条件的不同又分为好几种初学者的主要困惑就是分不清它们。θ连接在笛卡尔积结果中选出满足某个条件的元组。R ⋈_条件 S σ_条件(R × S)比如“学生表连接选课表条件是学号相同”Student ⋈_Student.SnoSC.Sno SC这里的条件可以是等值也可以是,等所以统称θ连接。等值连接θ连接的特殊情况连接条件是“某个属性等于另一个属性”。结果里两组连接属性列都会保留所以会出现两个长得差不多的列。自然连接Natural Join更严格。它不仅要求是等值连接还要求比较的属性名相同并且在结果中把重复的同名属性合并成一列。因此自然连接会自动去重那些“名称相同且值相等”的属性。比如Student和SC都有Sno属性自然连接Student ⋈ SC会自动按Sno进行等值连接而且结果里只保留一个Sno列。SQL里自然连接确实有对应语法SELECT * FROM Student NATURAL JOIN SC;但我建议实际开发中不要用NATURAL JOIN因为它隐式地按所有同名属性连接一旦表结构变了连接逻辑可能悄悄发生变化非常危险。日常写SQL还是老老实实写ON条件。3.2 交运算两种理解方式交运算表示“在R中也在S中”的元组。前面说过可以用差运算推导出来R ∩ S R - (R - S)也可以直接用R ∩ S要求两个关系模式相容。SQL对应是INTERSECT关键词MySQL不支持但可以用IN或 EXISTS模拟。交运算在实际查询里不算频繁但它和并、差构成了集合操作的三板斧在报表统计、数据比对时会用到。3.3 除运算关系代数里最抽象的运算除运算÷是很多人的噩梦但它的意义很明确表达“至少包含所有”这类全称量词查询。除运算解决的问题是当我们要找“跟某个集合中所有项目都匹配”的对象时用前面的操作都会很费劲而除运算能一步到位。形式上R÷S要求S的属性集是R属性集的子集。以经典的“学生-课程-选课”模型为例R是选课记录Sno, CnoS是“某门课程”的集合Cno。R÷S的结果就是那些“选了S中所有课程”的学生的Sno。一个具体例子选课表SCSno, Cno现在要找选了课程1和课程2两门课的学生学号。可以先构造一个临时关系TT里就是两行Cno1和2。然后做π_Sno,Cno(SC) ÷ T结果是选了这两门课的学生。整个过程等价于“学生选课集合包含目标课程集合”。SQL里没有直接对应的除法但可以用NOT EXISTS双重否定来写SELECT DISTINCT Sno FROM SC a WHERE NOT EXISTS ( SELECT 1 FROM T WHERE NOT EXISTS ( SELECT 1 FROM SC b WHERE b.Sno a.Sno AND b.Cno T.Cno ) );理解除运算有个技巧从“不满足”的角度反向思考。除法的结果是那些“即使做差运算也删不掉”的元素。具体来说R÷S的元组t要满足t与S的所有组合都在R中。换句话说如果把R中某个学生的所有选课记录拿掉再和S做笛卡尔积剩下的学生就不是结果只有那些S中每一门课都出现在该学生选课记录里的学生才能留在结果里。考试时除运算常和“至少、全部”放一起出大题这类题目确实有固定套路后面我会给一个示例来演示。4. 实战拆解如何把一句查询需求翻译成关系代数4.1 关系代数的思考顺序不是代码是思维流程很多同学做关系代数题目的困难在于SQL用多了思维方式被“SELECT...FROM...WHERE...”框住了。但关系代数的思考方式是完全不同的它是一种从内向外、层层嵌套的组合逻辑。我总结了一个三步走的思路第一步确定“关系源”。看这个查询需要哪些表每个表承担什么角色如果需要同时比较同一张表就要考虑重命名。第二步确定“筛选流水线”。哪些条件是在单表内部就能筛掉的用选择σ哪些条件是表之间的连接条件用连接⋈或笛卡尔积选择哪些条件需要在连接后再筛例如基于多个条件的组合判断。第三步确定“输出列”。只保留最终需要的属性列用投影π收尾。大多数关系代数答案的最外层都是一个π就是这个原因。4.2 经典例题一单表过滤 投影需求查询计算机系CS男学生的姓名和年龄。如果学生表Student里还有一个性别属性Ssex那么这道题非常直白π_Sname, Sage(σ_SdeptCS ∧ Ssex男(Student))整个过程就是先选择行横向过滤再投影列纵向裁剪。这里要提醒你注意操作顺序先σ后π。如果把π放前面把你想要的列提前投影出来再去筛Sdept和Ssex就会因为这两个属性被丢掉了而根本无法完成筛选。这就是为什么说“先选行、再选列”是关系代数表达式的基本规范。4.3 经典例题二两表连接 复杂条件需求查询选修了课程号为“C001”的学生的姓名。这个查询涉及两张表Student存Sname和SC存Sno和Cno。学生姓名在Student里选课记录在SC里所以需要连接。连接条件是学号相等同时过滤课程号为C001。表达式可以这样写π_Sname(σ_SC.CnoC001(Student ⋈_Student.SnoSC.Sno SC))翻译成人话就是先把Student和SC按学号连接起来得到一个“学生选课记录”的大表再从中筛选C001的选课记录最后输出姓名。SQL对应SELECT Sname FROM Student JOIN SC ON Student.Sno SC.Sno WHERE CnoC001;这里有一个值得琢磨的点条件是先连接再筛选还是先筛选再连接从关系代数的角度看先筛选再连接是更高效的因为减少了参与连接的数据量。所以把表达式优化一下π_Sname(Student ⋈_Student.SnoSC.Sno (σ_SC.CnoC001(SC)))这种“先缩表再连接”的思路其实就是数据库优化器经常做的“谓词下推”。你在写SQL时可能不需要手动做但理解这件事以后看执行计划就不费劲了。4.4 经典例题三自连接 差运算否定/全称查询需求查询选修了全部课程的学生的学号。这道题是关系代数的招牌题型处理方式可以分两种。方法一用除运算。设课程集合为Course投影出Cno选课记录为SCSno, Cno那么π_Sno, Cno(SC) ÷ π_Cno(Course)这个除法表达式非常简洁是除运算最经典的表现形式。方法二不用除运算用差运算反推。思路是“找出那些至少有一门课没选的学生再从所有学生中减去他们”。第一步构造“所有学生 × 所有课程”——所有可能的选课组合π_Sno(Student) × π_Cno(Course)第二步减去“已经存在选课记录”的组合得到“没选的组合”(π_Sno(Student) × π_Cno(Course)) - π_Sno,Cno(SC)第三步投影出这些没选全课程的学生学号π_Sno[ (π_Sno(Student) × π_Cno(Course)) - π_Sno,Cno(SC) ]第四步全体学生减去这些“有缺课”的学生就是选全了的学生π_Sno(Student) - π_Sno[ (π_Sno(Student) × π_Cno(Course)) - π_Sno,Cno(SC) ]这个方法看起来复杂但它展示了关系代数的“底层能力”——除运算能一步完成的事差运算加笛卡尔积也能一步步拼出来。考试如果要求不能用除运算这个差运算的写法就是标准答案。4.5 关系代数到SQL的翻译对照表学关系代数一个非常实用的技巧就是建立它和SQL的映射关系。我把常见运算的对应关系整理成一张表可以保存下来对照着看。关系代数操作含义SQL对应σ_条件(R)按行筛选WHEREπ_列(R)按列投影去重SELECT DISTINCTR ∪ S并集去重UNIONR - S差集NOT IN / EXCEPT / NOT EXISTSR × S笛卡尔积CROSS JOIN或FROM A,B不加WHERER ⋈_θ S带条件连接JOIN ... ON 条件R ⋈ S自然连接同名属性等值连接并去重复列NATURAL JOIN谨慎使用R ∩ S交集INTERSECTMySQL不支持ρ_别名(R)重命名关系/属性表别名/列别名ASR ÷ S包含全部S的元组NOT EXISTS双重否定这张表的价值不在于背下来而在于当你用SQL写一个看似复杂的查询时能在心里把它“翻译”成一棵关系代数树然后判断这个查询到底做了哪些操作、能不能优化、会不会有潜在的性能陷阱。5. 关系代数学习中的常见错误与避坑技巧5.1 选择与投影混着写顺序颠倒最常见的问题就是先把列投影掉了再去选择那些被投影掉的列导致表达式直接报错或者结果为空。我自己带过的学生里有不少犯过这个错误。你要记住一条铁律选择条件里出现的属性一定不能在选择之前就被投影丢掉。写表达式时可以先写σ再写π如果必须调整顺序一定确认σ用到的属性在π的输出集里仍然存在。5.2 笛卡尔积忘了带连接条件写连接运算时忘写连接条件在关系代数里就是R×S行数瞬间膨胀结果完全不是你想要的东西。在练习题里如果用了×却只有一个关系名、没有给出等价条件大概率是要扣分的。我的经验是凡是看到两个关系用×连接立刻问自己一句——“这两个表靠什么字段发生关联”这个关联条件就是连接条件。如果没有关联字段那基本可以判断这题不适合用×连接要么题目信息缺失要么应该用别的运算。5.3 除运算只会背定义不会套用除运算的难点在于它太抽象。我的建议是遇到“至少/全部/所有”这类全称量词查询时先判断两个问题被除关系是什么除关系是什么被除关系里通常有两个关键属性一个表示“主体”比如学生一个表示“项目”比如课程。除关系里只有一个“项目”列存放“必须全部满足”的集合。结果就是“主体”列的一个子集。比如“选了所有课程的学生的学号”主体是Sno项目是Cno被除关系就是SC(Sno,Cno)除关系就是Course投影出的Cno。这样一分析除法表达式自然就出来了。如果不放心还可以用“差运算法”去验证除法的结果对不对两个方法得到的结果应该一致。5.4 忘了重命名导致自连接失败自连接的场景下如果不给副本重命名就无法区分“表自身的左边和右边”。这是关系代数考试中的一个高频失分点。正确做法是使用ρ给每个副本起不同的别名并在连接条件里用“别名.属性”的方式指明出处π_a.Sno(σ_a.Snob.Sno ∧ a.Cno1 ∧ b.Cno2(ρ_a(SC) × ρ_b(SC)))写SQL时也是同理表别名是必须的。这种思维转换虽然需要一点时间但一旦习惯自连接就没有什么神秘的了。5.5 做题/备考的几条实操建议最后分享几个我刷题总结出来的经验无论是应对考试还是加深理解都有用第一刷题时一定要写完整表达式不要只写“思路”。很多同学心里明白但一写就出错。把σ、π、⋈这些符号写下来按部就班地推导错误率会大幅降低。第二做复杂的综合题时先画“关系代数的执行树”。把一层层嵌套展开成树状结构每一步对应一个操作检查每个操作的输入输出是否合理。这能帮你清晰地把握整条查询链路。第三把关系代数表达式转换成SQL再用SQL去验证结果。这是最有用的自检方式。比如你写了一个除法表达式翻译成SQL跑一下跟自己的逻辑预期比对不一致就说明哪里出了问题。反过来也一样用SQL构造同一个查询再翻译成关系代数表达式可以检验你是不是真的理解了运算的语义。第四善用教材配套的课后题和题库。数据库系统概论第六版这套书里的例题很有代表性特别是选课模型那套题吃透了以后考试里大多数关系代数的题型都能应付。另外网上也能找到很多高校的期末真题做的时候注意总结常见题型单表查询、两表连接查询、自连接查询、嵌套查询NOT EXISTS、除法查询基本就覆盖了90%的考法。写在最后的一点体会学了关系代数之后我最大的感受是它不会直接让你写出更快的SQL但会让你看懂数据库执行的底层逻辑。尤其是当你需要在索引设计、慢查询优化上做决策时能够下意识地说出“这个查询其实就是一次选择和连接索引应该建在连接字段上”这种能力就是关系代数思维带来的。所以别被那一堆符号吓住它们本质上就是几个简单的操作切行、切列、拼表、集合加减乘除。把这套思维练熟了你再看SQL、看查询计划会有一种豁然开朗的感觉。希望这篇文章能帮你把这扇门推开。