
1. 从SQL写得好到真正懂关系模型这三节内容的分量先问一个扎心的问题你写了两年SQL能熟练写各种JOIN、子查询、窗口函数但你知道自己每天都在用的SELECT、WHERE、JOIN在关系代数里对应的是哪几个运算吗如果你答不上来那这篇内容就是为你准备的。数据库系统概论里的2.2关系操作、2.3关系完整性、2.4关系代数这三节是整门课的分水岭。很多人学数据库一上来就抱着SQL语法啃建表、查询、索引玩得很溜但一遇到稍微绕一点的问题就卡壳——比如查出来所有选了全部课程的学生找出比所有计算机系学生年龄都小的其他系学生这类经典面试题能写出来的人不少但能说清楚自己为什么这么写的人不多。差距就在关系代数这一层。这三节内容解决的是三个层面的问题关系操作回答关系模型能做什么关系完整性回答数据凭什么可信关系代数回答查询到底是怎么算出来的。理解了这三件事SQL对你来说就不再是一堆语法规则而是一套有逻辑、有依据的数学表达。这篇文章我尽量按实际理解路径来走把理论和应用之间的缝隙填上不是照本宣科地复述教材而是讲清楚每块内容背后的设计逻辑和它在真实业务里的落点。2. 关系操作为什么教材反复强调一次一集合2.1 关系操作的最小单元决定了思维模式的转换关系操作说白了就是定义关系模型能对数据做什么动作。教材上列了增、删、改、查四类但重点全在查上。查询操作又分选择、投影、连接、除、并、差、交、笛卡尔积这几大类。你仔细看会发现这些操作有一个共同特征操作的对象和结果都是集合关系。一次一集合是关系模型和文件系统、层次模型、网状模型最本质的区别。文件系统里你要查一条记录得沿着指针一条条走一次拿一条这叫一次一记录。关系模型直接声明式地告诉你我要所有年龄大于20的学生数据库自己去决定怎么找你不需要关心存储细节。这种思维转换是从怎么取数据到声明你要什么数据的跨越。初学的时候容易忽略一件事关系操作是封闭的——操作的结果仍然是一个关系。这就意味着你可以对一个查询结果继续做查询嵌套着来。SQL里为什么能写子查询为什么能FROM一个子查询底层依据就是这个封闭性。没有这个性质SQL的表达能力会大打折扣。2.2 分组、排序为什么不算关系操作这里有个很经典的考点也是很多人学完一头雾水的地方教材里讲关系操作提了分组、排序但说它们不算纯关系操作。不是考试要背而是你得理解背后的原因。关系是一个无序的集合元组之间没有先后顺序这由关系的数学定义决定——集合本身就是无序的。排序给关系强加了一个物理顺序这个顺序不是关系本身的属性而是为了输出展示或者配合某些算法才存在的。分组同理它改变的是数据组织的粒度把多个元组聚合成一组组本身已经不是元组了不再满足关系定义中每一行是一个元组的约束。这个问题在实战中真的会碰见。MySQL 5.7里用GROUP BY做分组查询时SELECT后面出现的非聚合列在SQL标准里本来应该被限制但MySQL早期版本不报错随机取一行导致很多人写出看似能跑但结果随机的SQL。如果你理解分组不是关系操作、分完组后的结果集已经变了形态就不会写出这种有隐患的语句。2.3 关系操作能力评估关系完备性关系操作还有一种分类方式分为关系代数和关系演算两大类。前者是怎么一步一步算后者是描述你想要什么结果。两者的表达能力是等价的这就是著名的关系完备性。SQL语言融合了这两套思想的精华既有关系代数的操作风格也有关系演算的声明式表达。判断一个查询语言是不是关系完备的标准就是它的表达能力是否等价于关系代数。现实中几乎没有商业数据库会去严格证明自己的SQL方言满足关系完备性但这套评估框架的价值在于当你面对一个新出的查询语言或大数据查询引擎时你可以用关系代数的这套操作集合去对照看它缺了哪个操作、多出来什么能力快速判断它的表达能力强弱。我在接触一些新型查询引擎时就用这个框架快速评估过确实实用。3. 关系完整性保证数据可信的三大约束3.1 实体完整性每一行都得有名有姓关系完整性这一节讲三类完整性约束实体完整性、参照完整性、用户定义完整性。前两个是关系模型必须满足的由系统自动支持最后一个是应用层面自己定义的。理解这三类约束的关键不是背概念而是想明白一个问题如果约束不存在数据会乱成什么样。实体完整性说的是主属性不能取空值。主键就是用来唯一标识一条记录的如果主键为空这条记录就无法被准确找到和引用实体就不存在了。MySQL里你建一张表指定了PRIMARY KEY插入一条主键为NULL的数据直接被拒绝。很多人把这条当作理所当然没想过为什么。但你反过来想如果允许主键为空代码里做数据关联时拿到一个NULL都不知道该去找哪条记录那整个系统的数据语义就崩了。在实际业务里实体完整性还有一个容易被忽视的延伸问题主键并不一定只有一列。复合主键的情况下主属性里任何一列都不能为空。你想想一个订单明细表(订单ID, 商品ID)作为联合主键如果订单ID为空这条明细就没办法归属到任何订单如果商品ID为空就不知道卖的是什么这条明细就是一条幽灵数据。3.2 参照完整性外键不是摆设是数据关系的安全带参照完整性讲的是外键的约束规则外键的值要么为空要么必须等于被参照表中某个元组的主键值。教材上这段话很多人背得滚瓜烂熟但一到实际开发就觉得外键性能差公司不让用外键直接把这条约束扔了。这里得说清楚两件事。第一参照完整性是关系模型层面的概念不是只有数据库物理外键才能实现。你可以在应用层保证也可以通过触发器实现甚至靠开发者的自律实现——但后者通常不靠谱。第二外键约束真正的价值在于它把数据之间的依赖关系显式化、强制化了。删除一篇文章时如果还有评论引用它数据库会拒绝删除或者级联删除或者置空这个拒绝/级联/置空的取舍必须是你想清楚的业务决策而不是运行时才发现的一堆孤儿数据。参照完整性里最容易考也最容易懵的是三个动作的语义区分RESTRICT限制删除/修改、CASCADE级联删除/修改、SET NULL置空。什么时候用哪一个取决于你的业务语义。删除一个分类时分类下的商品怎么办如果商品必须属于某个分类那就RESTRICT不允许删如果商品允许无分类那就SET NULL分类删了商品还在如果商品跟着分类一起消失那就CASCADE。这个决策没有标准答案完全看业务。3.3 用户定义完整性把业务规则下沉到数据库用户定义完整性是应用层面自定义的约束常见的有非空约束、唯一约束、CHECK约束。概念不难但我想多说一句一个约束写在哪里直接决定了系统的数据质量底线。最典型的场景是状态字段。比如一张订单表有个status字段业务上只允许取待支付、已支付、已发货、已完成、已取消这五个值。如果应用层用枚举值管理但数据库字段没有CHECK约束那么一旦有人绕过应用层直接改数据库或者应用层代码有bug插入了已付这样的脏值数据就再也洗不干净了。数据库的CHECK约束太丑或性能有顾虑至少可以用ENUM类型或者触发器兜底。我在项目里见过太多应用层校验了就行然后被脏数据坑的例子经验就是能用数据库兜底的约束永远不要只靠应用层。这里还要提一种现代数据库才有的能力顺便兜个底。PostgreSQL里可以写非常复杂的CHECK约束甚至可以调用函数来校验这极大扩展了用户定义完整性的表达边界。MySQL 8.0.16之后才真正强制执行CHECK约束之前版本CHECK会被解析但被忽略——如果你还在用老版本MySQL得知道这条约束可能根本没生效。4. 关系代数一套完整算路的拆解4.1 集合运算并、差、交、笛卡尔积是怎么定义出来的关系代数的运算分两大类集合运算和专门的关系运算。集合运算把关系当集合处理包括并∪、差−、交∩、笛卡尔积×。教材上每个运算都给了定义式比如并运算R∪S { t | t ∈ R ∨ t ∈ S }。这个定义式的关键前提是并相容性两个关系的属性数量必须相同且对应属性的域必须一致。这个条件如果换个说法就是两个表的结构要能对上。SQL里的UNION对应并运算UNION ALL对应的是不去重的并。注意关系代数里的并运算自动去重因为集合不允许重复元素而SQL表允许重复行所以SQL的UNION默认去重、UNION ALL不去重这个差异是理解SQL和关系代数关系的重要缝隙。笛卡尔积是另一个必须吃透的运算——几乎所有复杂查询的底层都有它。R×S就是把R的每一行分别和S的每一行拼接。如果R有m行S有n行结果就有m×n行。听上去很暴力但连接运算JOIN本质上就是先做笛卡尔积再做选择。理解这一点你能明白为什么JOIN的性能和表的行数强相关也就能理解为什么DBA天天喊小表驱动大表。4.2 专门的关系运算选择、投影、连接、除专门的关系运算才是关系代数的灵魂四个核心运算的语义必须非常清晰选择σ按条件筛选行。σ_age20(Student)从Student里把年龄大于20的行挑出来。对应SQL的WHERE子句。选择的本质是取行的子集列的维度不变。投影π按列取属性。π_name,age(Student)只保留name和age两列。对应SQL的SELECT列清单。投影的本质是取列的子集而且会去重——这点和直觉不一样但集合不允许重复所以投影结果中重复元组会被消去。SQL里SELECT默认不去重这是SQL不完全是关系代数的最典型例证。连接⋈把两个表按条件拼起来。等值连接是连接条件为相等的特例自然连接则更进一步要求比较的列名相同并且把重复列去掉。自然连接的结果列数更少。写SQL时很少显式用自然连接NATURAL JOIN这个语法在MySQL里也有但实际用得少因为它的行为容易被表结构变化影响。这又是一个懂理论但不一定要全盘照搬到实践的例子。除÷关系代数里最劝退的一个运算。除运算解决的是查询涉及全部的语义问题找出选了全部课程的学生找出掌握了所有技能的人。教材上的定义式对初学者极其不友好涉及象集、商、余象这些概念。我在学习时找到的最直观理解方式R÷S的结果是那些在R中对应了S中所有值的侧的取值。举个具体的例子。选课表SC(学号, 课程号)课程表C(课程号)算SC ÷ C得到的是选了所有课程的学生学号。如果把S换成某几个特定课程(001, 002)SC ÷ 这个集合得到的就是同时选了001和002两门课的学生。这就是全部所有类查询的代数学表达。4.3 象集理解除运算的钥匙除运算难核心卡在象集这个概念上。象集的定义是对于关系R元组t在X上的取值x的象集记作Z_x { t[Z] | t ∈ R, t[X] x }意思是在R中与x相关的所有Z值组成的集合。用人话说一张表里某个学号对应的所有课程号的集合就是该学号在这张表上的象集。除运算要做的就是拿S的取值集合去和每个x的象集比大小。如果x的象集包含了S的所有值这个x就进结果。这个概念一旦通了SQL面题里的经典题——找出选修了全部课程的学生——就有了明确的分析路径。你不需要死记SQL写法只需要把关系代数表达式写出来SQL自然就能翻译出来。而且这类问题在考察时变化无穷比如选修了学号为20210001的学生所选修的全部课程的学生本质还是除运算只是被除数变成了一个更复杂的表达式。5. 关系代数表达式与SQL的实操对照表格是这套知识体系里最实用的干货我把自己当年对照学习时的经验总结成一张大表关系代数运算数学符号SQL对应核心差异与注意事项选择σ (Sigma)WHERE子句选择不改变列去重由SQL语义决定投影π (Pi)SELECT列清单关系代数投影自动去重SQL默认不去重并∪ (Union)UNIONSQL的UNION去重UNION ALL不去重差− (Minus)EXCEPTMySQL用NOT IN或LEFT JOIN模拟MySQL没有EXCEPT关键字得绕路交∩ (Intersection)INTERSECTMySQL用IN或JOIN模拟同上笛卡尔积× (Cross)CROSS JOIN或逗号连接行数成倍放大注意过滤条件等值连接⋈JOIN ... ON 等值条件列会重复需要手动选择列自然连接⋈NATURAL JOIN或手动等值连接去重复列实际开发最好不用NATURAL JOIN结构变动易出错除÷通常用NOT EXISTS或COUNTGROUP BY实现没有直接对应需要组合拳这张表的价值在于每个SQL查询背后都有一个关系代数表达式。写SQL卡住的时候先回到关系代数层面把操作的逻辑链条理清楚再翻译成SQL思路会清晰非常多。5.1 从关系代数到SQL的翻译实例我拿教材里最经典的题目来演示这条翻译路径。题目查询选修了全部课程的学生姓名。关系代数表达式可以写为π_Sname(Student ⋈ (SC ÷ π_Cno(Course)))翻译步骤第一步内层的π_Cno(Course)是所有课程号集合SQL就是一个子查询SELECT Cno FROM Course。第二步SC ÷ 这个集合语义是在SC中找出那些课程号覆盖了全部课程的学号。第三步翻译成SQL时除运算的经典实现是不存在一门课程这个学生没选过——这正好是NOT EXISTS的语义SELECT Sname FROM Student WHERE NOT EXISTS ( SELECT * FROM Course WHERE NOT EXISTS ( SELECT * FROM SC WHERE SC.Sno Student.Sno AND SC.Cno Course.Cno ) );这三层嵌套的SQL看着很绕但一旦你理解除运算的本质是全部包含语义这个SQL就非常自然了。反向NOT EXISTS是是否存在一门课你没选外面再套一层NOT就是不存在你没选的课等于全选了。5.2 常见查询的代数表达练习再把几个教材里必练的查询和它们的关系代数表达串一遍你会发现几乎所有查询模式都有固定的代数套路查询年龄不小于20岁的男学生的姓名π_Sname(σ_age≥20∧Ssex男(Student))查询选了数据库课程的学生的学号和姓名π_Sno,Sname(Student ⋈ σ_Cname数据库(SC ⋈ Course))查询一门课都没选的学生π_Sno(Student) − π_Sno(SC)查询选了全部课程的学生的学号π_Sno,Cno(SC) ÷ π_Cno(Course)查询至少选了20210001号学生所选修的全部课程的学生学号π_Sno,Cno(SC) ÷ π_Cno(σ_Sno20210001(SC))第五个题目是一个很好的思维训练除法的右边不只是所有课程而是某个特定学生选的课程。这题的SQL写法和全选那题很像唯一的区别是把Course整表换成了那个学生的选课记录。5.3 实践中怎么验证结果对不对初学关系代数时最容易出现的问题表达式写出来了但不知道对不对。这里分享一个我常用的小技巧——用一个极小的数据集手算验证。比如上面第五题造一个只有3个学生、4门课、每人选2-3门课的小数据集手动在纸上做一遍除法看看结果是什么再用SQL跑一遍验证。手动计算让你真正理解运算逻辑SQL跑出来能帮你确认关系代数表达式和SQL翻译是否一致。这个方法笨但对于理解除运算特别有效我在带新人时也一直推荐这么做。6. 为什么现在还要学关系代数三个被低估的价值6.1 面试中的硬通货数据库面试里全部/至少/不存在类问题是高频考点。但面试官真正想看到的不是你能背出SQL写法而是你能不能用关系代数把问题表达出来然后自然地翻译成SQL。我在面试候选人的时候通常会给一道查询选修了全部课程的学生的题能直接写出嵌套NOT EXISTS的人不少但能说清楚自己为什么这么写、底层对应什么运算的人不多。这两者之间的差距就是会写和理解的差距这直接影响后面处理复杂数据问题的能力。6.2 复杂查询的思维脚手架关系代数最大的价值不是提供具体的SQL语法而是提供了一套思考数据操作的方法论。当你面对一个复杂的查询需求第一反应不应该是这条SQL怎么写而应该是我需要做哪几步操作、每一步的输入输出是什么。关系代数表达式天然就是查询计划的说明文档把一个大查询拆成几个小运算的组合每个小运算的语义都极其清晰。这几年很多团队在做数据分析和报表开发经常要写几百行的SQL没有这套拆解思路写出来的SQL很容易逻辑纠缠、难维护。6.3 查询优化与执行计划的理解基础数据库优化器在执行SQL之前会把SQL翻译成关系代数表达式再做等价变换比如把选择下推、把投影提前最后生成执行计划。如果你能看懂EXPLAIN的输出但理解不了为什么MySQL会先做某个条件下的过滤再做JOIN那就是因为没有关系代数层面的操作重排概念。查询优化的本质就是在保持结果不变的前提下通过调整关系代数运算的顺序和实现方式降低中间结果的大小和计算开销。这不是教材上的一句空话而是在处理慢查询时实实在在需要的核心知识。7. 学习建议与易混点排查清单最后整理一份自查清单学完2.2到2.4这几节你可以对照着检验自己是不是真的理解了这些都是我在实际工作和带新人时反复遇到的易混点7.1 易混点一自然连接和等值连接的区别等值连接是比较条件为相等的连接结果会保留两个表里的连接列重复列不消除。自然连接是等值连接的特例要求比较列名相同并且结果会把重复列合并掉。举个例子Student和SC用Sno做等值连接结果里有两个Sno列或Sno在结果里出现两次自然连接只保留一个Sno。SQL开发中最容易出问题的地方就在这如果你用USING (Sno)而不是ON Student.Sno SC.Sno结果里Sno就只出现一次SELECT *时的列数完全不一样。7.2 易混点二除运算和NOT IN的关系很多人试图用NOT IN写全部类查询但语义上NOT IN表达的是不属于某个集合和除运算的包含全部完全是两码事。没选过任何课的学生可以用NOT IN但选了所有课的学生不能。前者是差运算后者是除运算。这个区分搞清楚了全部类题目就不会再写错。7.3 易混点三关系代数去重和SQL不去重的冲突关系代数中集合运算自动去重但SQL表允许重复行。这个差异导致了很多看起来理论正确但SQL结果比预期多的情况最典型的就是JOIN产生重复行。当你从关系代数角度觉得自然连接后应该每条记录唯一时SQL里的JOIN可能因为一对多关系产生重复——这在多对多关联查询中尤其常见。解决办法是理解关系模型的关系和SQL表的多值集合语义差异用DISTINCT在有需要时显式去重。7.4 复习路径建议如果时间有限这几节内容建议按概念→语义→表达式→SQL翻译→手算验证的顺序推进。概念和语义部分关系操作、三类完整性以理解为主能用自己的话讲清楚即可关系代数部分必须动笔写——每道例题先在纸上写关系代数表达式再翻译成SQL再用小数据集验证。我自己就是靠这个流程把关系代数这块啃下来的写SQL的逻辑也因此清晰了很多。数据库系统概论这门课到2.4关系代数这里算是真正上强度了。前面两章建表、画ER图都还算直观从关系代数开始需要抽象思维了。但咬咬牙把这块啃下来后面学SQL、学查询优化、学事务管理会顺很多。如果这期间有哪块概念没转过来欢迎评论区讨论我看到都会回的。