数据库第6章总结——关系数据理论这章在数据库课程里的位置挺有意思前面几章教你怎么建表、写SQL到了这一章突然开始讲“为什么不能这么建表”。很多同学学到这儿会觉得抽象函数依赖、范式、模式分解这些概念一套一套的背下来容易用起来糊涂。我的看法是这一章恰恰是整个数据库设计里最值得花时间琢磨的内容它回答了一个非常实际的问题一张表设计成什么样才不会在后期维护时把自己坑死。这章能解决什么问题简单说就是通过一套形式化的规则判断表结构是否合理能不能在不丢失信息、不破坏依赖的前提下把大表拆成更规范的小表。适合谁来读如果你在准备考试那候选码求解和范式判定是绕不开的得分点如果你在学校做过课设或者在公司写过业务表你会发现很多“说不清哪里难受”的表结构问题在这章里都有明确答案。这篇总结我就按自己的理解把整个理论框架拆开讲一遍配上例子和做题思路尽量让每个概念都能落地。1. 这一章到底在解决什么问题1.1 先说清楚“好表”和“烂表”的差别我见过不少同学写建表语句第一反应是“字段够用就行”于是把一堆信息塞进一张表里。举个例子一张学生选课表学号、姓名、系别、系主任、课程号、课程名、成绩这张表能跑吗能。但是只要往里面插入一条“某课程还没人选”的数据就会因为学号为空而插不进去——明明课程是存在的却没法记录。再比如某位同学转系了他的所有选课记录都要跟着改系主任只要漏改一行数据就矛盾了。这些都是“数据冗余”和“更新异常”的典型表现。关系数据理论要做的就是把这些现象抽象成两个问题第一表里有没有不该存在的依赖关系第二这种依赖会不会导致插入、删除、修改时出现异常。只要把这两点分析清楚表设计是否合理就有了客观判据不再凭感觉。1.2 五个核心概念的关系千万别搞混函数依赖、候选码、主属性、范式、模式分解这五个词几乎贯穿整章。我的理解是这样的函数依赖是分析的基本工具它描述属性之间的约束关系候选码和主属性是依赖分析中的定位点用来判断哪些属性是“身份的象征”范式是一套分级标准每一级都在消除特定类型的坏依赖模式分解则是把不合格的表改造成合格表的手段而分解是否成功要看两个指标——无损连接和保持依赖。这五个概念是层层递进的关系不是五个独立考点。考试里很多题是组合出现的比如“给出一个关系模式和函数依赖集判断它属于第几范式再分解到3NF并验证无损性和保持依赖性”。不会拆函数依赖后面全卡住。1.3 为什么这套理论在实际业务里依然不过时有人会觉得这都是关系模型刚提出时候的理论现在用NoSQL、用ORM是不是用不上了我的看法是只要你还用关系型数据库存业务数据范式理论就有用。ORM只是把SQL语句生成自动化了表结构还是要人设计。我在实际项目里见过不少“为了查得爽而故意反范式”的设计比如在订单表里冗余存一个用户昵称。这种设计的代价是用户改昵称时必须记得同步更新所有历史订单里的冗余字段一旦漏掉运营导出报表时就会看到新旧昵称并存。什么时候可以接受这种冗余当读多写极少、且团队有严格的同步机制时才值得考虑。绝大多数业务表规规矩矩按3NF或者BCNF来设计后面会省很多麻烦。这章理论的工程价值就在这里它给了你判断“什么时候该拆、什么时候不该拆”的标尺。2. 函数依赖整套理论的基石2.1 从“属性之间的约束”理解函数依赖函数依赖的定义是在关系模式R(U)中如果属性集X的每一个取值都对应唯一一个Y的取值就说X函数确定Y记作X→Y。这个定义初看很绕我用大白话解释学号确定姓名是因为一个学号只对应一个学生不可能一个学号有两个姓名但是姓名不能确定学号因为重名的存在会让一个姓名对应多个学号。这里注意一个理解陷阱到底什么算“对应”必须是在所有可能的合法数据上都成立而不是当前表里碰巧成立。比如当前表里所有学生都是同一个系学号→系别确实成立但如果换一批学生就不一定了。做题时函数依赖一定是题目给你的已知条件或者能用公理推出来的结论不能从某张具体表的几行数据里瞎统计。2.2 Armstrong公理和属性闭包的计算方法函数依赖能推导靠的是一套公理系统叫Armstrong公理。核心有三条自反律、增广律、传递律。自反律是说如果Y是X的子集那X→Y一定成立增广律是说如果X→Y成立那XZ→YZ也成立传递律就是X→Y且Y→Z能推出X→Z。光背这三条不够真正做题时经常用到的是由它们推导出的几个规则合并规则X→Y且X→Z可以推出X→YZ。分解规则X→YZ可以推出X→Y和X→Z。伪传递规则X→Y且WY→Z可以推出WX→Z。这些规则的价值在于计算属性集闭包给定属性集X通过函数依赖集F能推出的所有属性集合记作X⁺。求闭包的方法是“逐步扩展法”先让闭包等于X本身然后反复扫描F只要发现某个依赖的左部已经被当前闭包包含就把它的右部加入闭包直到闭包不再变化。举个完整例子。R(U) {A, B, C, D, E}F {A→B, B→C, BC→E}求A⁺。第一步闭包初始为{A}。扫描FA→B的左部A在闭包中加B闭包变成{A, B}。再继续扫描B→C的左部B在闭包中加C闭包变成{A, B, C}。继续扫描BC→E的左部BC都在闭包中加E闭包变成{A, B, C, E}。再扫一遍没有任何新属性可加所以A⁺ {A, B, C, E}。这里有个技巧每加入一个新属性都要重新从头扫描一遍F不要只扫一遍。漏扫是算错闭包最常见的原因。2.3 最小函数依赖集后面分解要用它最小函数依赖集概念也很重要它的作用是去掉函数依赖集里的“水分”。所谓最小要满足三个条件每个依赖的右部是单一属性每个依赖的左部没有多余属性整个依赖集里没有多余的依赖。求最小函数依赖集的步骤我建议按三条来走。第一步把每个依赖的右部拆成单属性比如A→BC拆成A→B和A→C。第二步逐个检查每个依赖的左部看能不能删掉某个属性还不影响闭包。第三步逐个检查每个依赖本身看删掉后函数依赖集的闭包是否发生变化不变化就删。这个最小化过程在模式分解的合成法中会用到我后面讲到3NF分解时会再详细演示。现在你只要记住一个函数依赖集可能有多个最小覆盖但每个最小覆盖包含的依赖个数是固定的求解过程有标准套路不能凭感觉删。我在教学中发现很多同学在第二步左部约简时犯迷糊核心原因是没理解“闭包不变才是删不掉”的判据。判断本质是验证某个依赖能否被其他依赖推导出来不能只看它“看起来没用”。3. 范式递进从第一范式到BCNF3.1 第一范式到第三范式的判定逻辑范式是从低级到高级的递进关系每一层都建立在前一层的基础上。第一范式1NF要求所有属性都是原子的不可再分。这个标准很好理解表里不能出现“一个单元格存多个值”的情况。比如把“电话号码”字段存成“010-12345678;13800000000”这就违反了1NF。实际工程里违反1NF的设计还能靠拆字段解决算不上大问题所以考试很少在这里卡人。第二范式2NF是在1NF基础上消除非主属性对候选码的部分函数依赖。什么是部分函数依赖就是某个候选码是组合键而某个非主属性只依赖组合键的一部分。举个例子关系模式R(学号, 课程号, 成绩, 学分)候选码是(学号, 课程号)。成绩完全依赖于整个(学号, 课程号)但学分只依赖于课程号这就是学分对候选码的部分函数依赖。这会造成什么后果某个课程还没人选学分就存不进去选课人数变化不影响学分却要反复重复存储造成冗余。第三范式3NF要求消除非主属性对候选码的传递函数依赖。传递依赖的形式是X→YY→Z其中Y不是候选码。经典例子R(学号, 系别, 系主任)候选码是学号。学号→系别系别→系主任所以学号→系主任是传递依赖。这会导致系主任改名时要改动多行某个系还没招生时系主任信息就无处存放。3NF本质上是在说非主属性应该只描述它所属的实体不能把另一个实体的属性混进来。3.2 BCNF把箭头指向检查到底BCNFBoyce-Codd范式是一个更严格的3NF。它要求对于关系模式里的每一个非平凡函数依赖X→YX都必须包含候选码也就是X必须是超码。这个定义很多同学背了但不懂我换个说法BCNF要求所有函数的“左部”都必须有资格当主键。关键区别在这里3NF允许“非主属性→非主属性”的情况吗不允许3NF已经消灭了非主属性到非主属性的传递依赖。但是3NF允许“主属性→非主属性”吗也不允许。真正被3NF放过、却被BCNF抓住的是“主属性依赖于非主属性的一部分”这类情况。让我想一个具体的例子。假设有关系模式R(A, B, C)函数依赖为A→B和B→C。候选码是A所有属性都是主属性因为候选码只有一个属性AB和C是非主属性。这里B→C是传递依赖不满足3NF。这个例子说明B→C中有非主属性作为决定因素3NF都过不了。更典型的是R(A, B, C)F {A→B, B→A, A→C}。候选码有两个分别是A和B。非主属性只有C每个非主属性C对候选码都不存在部分或传递依赖所以满足3NF。但函数依赖B→A中B虽然本身是候选码却要检查“每个依赖左部是否包含某个候选码”。这里B确实是候选码所以还是满足BCNF。需要找一个左部不是超码的例子比如R(A, B, C, D)F {AB→C, C→D, D→C}。候选码是ABC和D是主属性因为它们出现在候选码AB形成过程中的依赖右部其实C和D没出现在候选码里因为候选码是ABAB的闭包是否能覆盖CDF中AB→CC→D所以AB闭包含C和D候选码确实是AB。但C→D中C不包含AB不是超码所以不满足BCNF。C→D对非主属性D也是传递依赖所以3NF不满足其实主属性怎么定义要看所有候选码AB是唯一候选码所以只有A、B是主属性C、D是非主属性。C→D实际上是非主属性→非主属性的传递依赖所以3NF也不过。看来BCNF超出3NF的经典例子通常是“候选码本身是多个且彼此之间存在交叉依赖”。比如R(A, B, C)F {AB→C, C→B}。候选码是AB和AC因为AC闭包AC BABC所有属性都是主属性满足3NF。但函数依赖C→B中C不包含候选码不满足BCNF。这就是一个标准例子。我记得这道题非常典型考试常考因为所有属性都是主属性3NF会放过它但BCNF能识别出C→B带来的更新异常当C确定B时如果一条记录里C对应B改了会牵连其他记录。BCNF的实际意义就是抓这种“主属性之间互相牵扯”的隐蔽问题。3.3 四种范式的判定顺序表我整理了一个快速判定流程做题时可以按这个顺序走第一步看有没有非原子属性有则属于1NF以下。第二步找出所有候选码区分主属性和非主属性。第三步检查非主属性是否对候选码存在部分依赖存在则不到2NF。第四步检查非主属性是否对候选码存在传递依赖存在则不到3NF。第五步检查每个非平凡依赖的左部是否都包含候选码只要有一个不满足就不到BCNF。这个流程最大的价值是帮你避免“一上来就猜第几范式”。很多同学喜欢扫一眼关系模式就报答案正确率完全靠运气。按流程一步步推理稳很多。4. 多值依赖与第四范式4.1 多值依赖是什么违不违反4NF多值依赖比函数依赖抽象一些。函数依赖说的是属性Y由X唯一确定而多值依赖说的是属性Y的取值集合与X绑定和Z属性无关。记作X→→Y。最经典的例子是课程、学生与参考书的关系。假设一门课程有多名助教每名助教多本参考书关系模式R(课程, 助教, 参考书)主键是三元组。这个表里如果存储“课程A有两名助教x、y参考书p、q”会变成四行数据x-p、x-q、y-p、y-q。这里没有任何函数依赖主键是三个属性的组合键非主属性没有所以它满足BCNF。但数据冗余非常明显参考书列表依附于助教彼此独立产生了笛卡尔积。这个表确实满足BCNF但设计得很糟糕。多值依赖的理论正是用来处理这种“隐含的笛卡尔积”问题。当X→→Y成立且X→→Z不成立时最好把Y和Z拆到两张表里去这就引出了第四范式4NF。4.2 4NF的判定与分解策略第四范式的定义是在关系模式R中如果存在非平凡的多值依赖X→→Y则X必须包含候选码。换句话说多值依赖的左部也必须是超码否则就得继续拆分。上面助教参考书的例子课程→→助教成立课程并不包含候选码候选码是全属性组合所以不满足4NF要分解成R1(课程, 助教)和R2(课程, 参考书)两张表。分解之后每张表内部的多值依赖要么消失要么左部变成了候选码。这才是符合直觉的设计。不过说实话考试里对4NF的考查频率远低于3NF和BCNF更多的场景是让你判断“给定依赖是不是多值依赖”以及“是否满足4NF”。我的建议是掌握定义和上面这个例子就够用不必花太多时间深挖。如果题目考到记得先判断是不是函数依赖再判断是不是多值依赖别把两种依赖混为一谈。5. 模式分解的两个硬指标无损连接与保持依赖5.1 为什么分解不能随便拆把一张表分解成多张表听起来简单但是拆错了会出大事。拆错有两种表现一种是把信息丢了比如原来能查到“张三选了数据库课”拆完之后两张表没法关联出这条记录这叫有损分解另一种是约束丢了原来有个函数依赖A→B在维护数据正确性拆完之后任何一张表里都检查不了这个约束只能靠应用层兜底这叫破坏函数依赖。理想的分解必须同时满足两个条件无损连接性和保持函数依赖。这也是考试里验证分解正确性的两个必查项。分解要能达到3NF通常这两个性质可以同时保住但如果强拆到BCNF有时会面临“保住无损就得牺牲某个依赖”的取舍实际工程里遇到这种情况要慎重决定。5.2 无损连接判定的表格法判断分解是否无损最稳妥的办法是表格法也叫追查法。具体步骤我写成下面这样建一张表格行是分解后的每个关系模式列是原关系模式的所有属性。对于每个格子如果某属性属于该行对应的关系模式就填上属性小写符号否则记为无符号。不断应用函数依赖集F如果某函数依赖X→Y表格中有两行在X列上符号相同就把这两行在Y列上统一符号。优先把无符号改成有符号如果有符号改成和对方一致的符号。重复直到表格不再变化。如果某一行全部变成有符号就是无损分解否则有损。我拿R(A, B, C)举例F {A→B, C→A}分解为R1(A, B)和R2(A, C)。初始表格R1行A、B有小写符号C无R2行A、C有小写符号B无。F中A→B两行的A列符号都是a于是把R2行的B从无改成和R1行的B一致的小写符号。表格第一行已经全有符号判断为无损分解。这个例子比较简单属性多的题目按同样的流程走就行关键是每一步都要重新扫描F。5.3 保持函数依赖的判断方法判断分解是否保持函数依赖标准做法是这样的把所有分解后关系模式上的函数依赖合起来得到一个新依赖集F′然后验证F中的每个依赖是否都能由F′的闭包推出。换句话说F中每个依赖X→Y要满足Y被包含在关于F′的X闭包里。实际操作时对每个依赖X→Y先看X的闭包在F′下能推出哪些属性再看Y是否包含在里面。不需要把F′里所有依赖的闭包都求一遍只需要针对F里的每个依赖验证。这里我踩过一个坑初学者经常把“F′的闭包和F的闭包相等”理解成“两个集合包含的依赖一模一样”其实不需要一模一样只要F′能从数量更少的依赖推出和F相同的结果就够了。这也是最小依赖集概念派上用场的地方。6. 实战范式判定与候选码求解的完整流程6.1 候选码求解的LRN分类法候选码的求解是这章的基本功很多范式判断题的第一步就是要先找候选码。我用的方法是LRN分类法L类只在函数依赖左部出现从未在右部出现。L类属性一定属于每个候选码。R类只在函数依赖右部出现从未在左部出现。R类属性一定不属于任何候选码。N类左右部都未出现的属性。N类属性一定属于每个候选码。LR类左右部都出现过的属性需要进一步试探。操作步骤先把L类和N类属性合并求这个属性集的闭包。如果闭包已经覆盖全属性那这个组合就是唯一候选码。如果没覆盖全就把LR类属性逐一加入用穷举方式求闭包找到所有能覆盖全属性的最小组合。来做一个经典题。R(U) {A, B, C, D, E}F {AB→C, C→D, D→A}。第一分类L类属性只有BAB左部C、D、A都出现在右部或左右部R类是E只在右部E没出现属于N类这里E出现在关系模式里但没有任何依赖涉及它所以属于N类。那么B和E属于每个候选码。先求(BE)的闭包BE在F下推不出任何新属性因为没有任何依赖左部是B、E或BE。所以BE不是候选码。接着逐个加LR类属性LR类有A、C、D。先试(A, B, E)AB→CC→DD→AA已存在所以闭包是{A, B, C, D, E}覆盖全属性(A, B, E)是候选码。再试(B, C, E)C→DD→AAB→C中B有但A没有所以闭包是{A, B, C, D, E}也覆盖全属性(B, C, E)是候选码。再试(B, D, E)D→AAB→CA有B有所以C有闭包是{A, B, C, D, E}(B, D, E)也是候选码。所以该关系模式有三个候选码。这道题做完你会发现N类属性E在每个候选码中都出现这就是“N类必属于候选码”的含义。做这类题耐心求闭包基本不会错。6.2 以一道真题走完范式判定全程我把上面的R继续往下推进来判断它的最高范式等级。关系模式R(U)候选码是(A,B,E)、(B,C,E)、(B,D,E)。主属性是A、B、C、D、E中的哪些在这些候选码里出现的所有属性都是主属性所以A、B、C、D、E全部是主属性没有非主属性。没有非主属性当然不存在非主属性对候选码的部分函数依赖和传递依赖所以R满足3NF。再检查BCNF需要确认每个依赖的左部是否都是超码。F {AB→C, C→D, D→A}。依赖AB→C左部AB它是否包含某个候选码AB包含A和B但是(A,B,E)才是候选码E不在AB里所以AB不是超码。因此R不满足BCNF。这种题在考试里几乎年年见先找候选码再判主属性最后定位范式。套路固定刷两三道就能熟。6.3 3NF无损且保依赖分解的合成法如果需要把R分解到3NF并且要求同时保持无损和依赖标准做法是“合成法”。步骤是这样求F的最小函数依赖集。把左部相同的依赖合并成一个关系模式。每个关系模式的属性集就是分解后的一个子关系。如果分解得到的任何一个关系模式都不包含原关系的候选码就增加一个包含候选码的关系模式这一步是为了保证无损连接。检查并去掉被其他关系模式包含的冗余子关系。回到上面那个R先把F最小化。F {AB→C, C→D, D→A}每个依赖的右部都是单属性。检查左部有没有多余属性AB→C里从B→C推不出C从A→C也推不出C所以AB没有多余属性。检查有没有多余依赖去掉D→A之后F闭包会变化所以保留。因此最小函数依赖集就是原来的三个依赖。按左部分组得到三个关系模式R1(A, B, C)R2(C, D)R3(D, A)。每个子关系都不包含原关系的候选码(A, B, E)、(B, C, E)、(B, D, E)中的任何一个所以补充一个R4(B, E)。最终分解是R1、R2、R3、R4。这个分解满足3NF同时保依赖和无损。至于BCNF的分解通常采用逐步分解法找出违反BCNF的依赖不断拆分这里不再展开但核心思想是一样的每次拆表时都要判断剩下的依赖是否还满足BCNF直到不能再拆为止。7. 考场与工程场景中的高频坑点7.1 做题时常见的几个坑第一个坑是“用表格数据证明函数依赖存在”。函数依赖是语义层面的约束不是统计结果。某次查询发现学号没有重复就断言学号→姓名这在理论上是不严密的考试里更是直接送命。所有依赖必须由题目给出的F明确表示或推导。第二个坑是“候选码求解漏N类属性”。求候选码时先把L类和N类拎出来求闭包很多人把N类漏了导致后面的分解缺胳膊少腿。第三个坑是“模式分解后忘记检查冗余子关系”。合成法分解出的子关系中如果有一个的关系属性是另一个的子集冗余的那个要删掉不删除会多出一张没用的表。第四个坑是“无损判定填表不迭代”。表格法填了几轮就放弃继续扫依赖了。记住只要表格上一轮发生了变化就一定要从头再扫一遍所有函数依赖。7.2 实际业务建模中的对应经验我把这章的知识用在实际建表时的体会分享给你。第一建表前先列函数依赖清单。不要一上来就画ER图、写CREATE TABLE先老老实实列出业务里的函数依赖关系。比如用户表和订单表先写“用户ID→用户姓名、用户手机号”再写“订单ID→用户ID、订单金额、下单时间”。写清楚之后哪些字段放哪张表基本一目了然。第二警惕“多值属性错误建模”。比如一张商品表想同时存“商品图片URL”和“商品标签”如果都用字符串存多个值其实就是违反了1NF如果拆成关联表就涉及多值依赖处理。我在实际项目里处理商品标签通常单独建一张标签表而不是在商品表里用逗号拼接。除非这个字段永远不会被单独查询、也绝不会参与统计否则看似省事的冗余最后都要付出代价。第三反范式化要明码标价。我已经说过工程里确实存在故意冗余的合理场景比如报表宽表。但每次冗余之前都要问三个问题谁来维护这个冗余字段更新不一致时怎么发现这个冗余能带来多大的性能收益三个问题都能回答再动手。回答不了就老老实实用范式设计。第四ORM框架的表映射不会自动帮你做范式检查。用框架只能省写SQL的时间不能帮你识别表结构问题。我见过一个项目把所有业务字段都塞进一张“对象属性表”用key-value形式存确实很灵活但查询统计异常痛苦。这个本质上是抛弃了关系模型的红利把数据库用成了文档存储还不支持文档数据库的查询能力。7.3 一个快速自查清单我把这章的要点压缩成自查清单做题、设计表之前都可以过一遍每个非主属性是否完全依赖于候选码有没有只依赖组合键中一部分的情况每个非主属性是否只描述候选码标识的实体有没有把别的实体的属性塞进来的情况每个函数依赖的左部是否都是超码如果不是这个依赖就是隐患。如果要拆表拆完后能不能通过连接原样还原数据所有函数依赖是否还在某个子表里能被检查存冗余字段了吗如果存了维护和校验方案是什么这套思路我从备考时一直用到工作里最大的感受是它能把“这个表设计得不好”从玄学变成一门可以论证的技术活。第6章的知识刚学时觉得理论性强但用久了会发现它其实是对业务建模经验的系统化提炼。我建议学这章时不要只背定义拿自己的课设表、实习时写过的业务表来练手多做几次范式分析比刷十道题都管用。
阅读完成 · 觉得有帮助?