
1. 为什么函数依赖、码、多值依赖是“牵一发动全身”的核心1.1 一张“病态表”引发的痛点先别急着背定义我用一个很多人都见过的真实场景开场。假设某公司人事系统里为了图省事把员工、部门、岗位一股脑塞进一张表员工表员工编号、员工姓名、部门编号、部门名称、岗位编号、岗位名称、办公地点填上几行数据问题立刻冒头同一个部门编号 D01 在二十条员工记录里重复出现对应的“研发部”也跟着重复二十遍。这时候你想改一下部门名称就得把所有相关记录全部扫一遍手一抖漏改两条同一部门两种叫法报表直接对不上。更麻烦的是新成立一个部门还没有招人因为员工编号是主键你连这条部门信息都没法先存进去部门只能“等员工来了才存在”。这个表到底坏在哪表面看是“信息重复”本质上是因为属性之间存在依赖关系而我们没有把它处理好。部门编号决定部门名称岗位编号决定岗位名称这些依赖导致了冗余。数据库系统工程师考试里反复强调的函数依赖、码与多值依赖说白了就是研究这类问题的一套系统方法论。后面所有范式判断、关系模式分解、ER图转关系模式全都要站在这一层基础上。这块地基如果是虚的后面做真题会越做越心虚。1.2 三大概念在考试中的真实分量我见过不少备考的朋友把精力全押在SQL优化、索引原理上觉得函数依赖这种“理论概念”背一背就行。结果真拿到真题碰到“求候选码”“判断该关系模式属于第几范式”“是否存在多值依赖如何分解到4NF”这类题直接卡壳。原因很简单这些题不是靠记忆能解决的它需要你按步骤推导一步错步步错。从考试分布看函数依赖、码、多值依赖和规范化设计通常能占到数据库设计部分相当比例的分数上午选择题会涉及下午案例分析更是高频考点。更重要的是这部分是“方法论型”知识学会了之后后面看ER模型转换、关系模式优化、数据仓库设计都顺了。所以下一节开始我会用能直接上手做题的方式把这三块逐个拆开讲透。2. 函数依赖一切规范分析的地基2.1 先把它理解成“属性间的契约”函数依赖的定义很简洁在关系 R 中如果属性集 X 的每一个取值都能唯一确定属性集 Y 的一个取值就说 Y 函数依赖于 X记作 X→Y。举个例子身份证号→姓名你看到一个身份证号就能确定一个唯一的姓名反过来姓名→身份证号就不成立因为同名的人太多了。学的时候很多人会掉进一个坑下意识地把“现实中的常识”当成函数依赖。函数依赖必须由关系模式中的语义约束决定不能靠猜。比如在一个选课关系里如果不强制规定“一个学生只能选一门课”那你不能因为碰巧看到几行数据很像一一对应就写下 学号→课程号。考试题目里给的函数依赖集合 F就是已知的业务规则所有推导都要以 F 为唯一依据。还有一类是平凡函数依赖。如果 Y 是 X 的子集比如 X→X或者 X→X∩Y这种就是平凡的没什么信息量。在判断范式、计算闭包时平凡依赖经常被忽略但偶尔也会成为干扰项需要会识别。2.2 三类重要依赖完全、部分、传递函数依赖按照“依赖的质量”可以分成三类这三类是判断 2NF、3NF 的关键。完全函数依赖Y 依赖于 X且不依赖于 X 的任何真子集。比如学号、课程号→成绩只有两者一起才能决定成绩把学号单拎出来决定不了成绩把课程号单拎出来也决定不了这就是完全依赖。部分函数依赖X→Y 成立但 X 的某个真子集也能推出 Y。还是拿学号、课程号举例子如果加一个 学号→姓名那在学号、课程号→姓名 这个依赖里姓名只依赖学号是部分依赖。这种依赖就是 2NF 需要消灭的对象因为它造成了大量数据冗余。传递函数依赖X→YY→Z并且 Y 不能推出 X那么 X→Z 就是传递依赖。典型例子学号→班级编号班级编号→班级名称那学号→班级名称就是传递依赖。虽然学号确实能决定班级名称但中间绕了一道班级名称存了多份改起来也麻烦这就是 3NF 要处理的问题。我用一个生活类比帮你记完全依赖就像“两个人一起签字才能生效的合同”缺一个都不行部分依赖是“一个人已经能拍板另一个人只是陪签”传递依赖是“你托熟人办事但熟人的熟人才是真正说了算的那个”。考试中判断范式等级本质上就是在看表里有没有这三种“不健康的依赖”。2.3 Armstrong公理推理的“铲子”函数依赖之间可以互相推导Armstrong 公理就是这套推导规则的基础考试中偶尔会让你判断“以下哪个函数依赖可以由给定依赖推出”。三条基本公理自反律如果 Y 是 X 的子集那么 X→Y。这个很直观属性集包含自身的一部分自然能“决定”它。增广律如果 X→Y那么 XZ→YZ。给左右两边同时加上同样的属性依赖仍然成立。传递律如果 X→YY→Z那么 X→Z。这就是依赖的链式推导。由这三条还能推出几条常用的扩展规则合并规则X→Y 且 X→Z则 X→YZ。分解规则X→YZ则 X→Y 且 X→Z。这相当于一个依赖可以拆成多个。伪传递规则X→YWY→Z则 WX→Z。做题时这些规则最常见的用法是判断“X 闭包里到底包含哪些属性”所以闭包和公理是配套使用的下一节就讲闭包。2.4 属性集闭包判断依赖是否成立的“计算器”闭包是这一章的硬核工具。给定属性集 X在依赖集合 F 下所有能被 X 确定的属性的集合称为 X 的闭包记作 X。计算闭包的算法很简单反复“滚雪球”初始化 X X。扫描 F 中的每条依赖如果某条依赖的左边属性全部都在当前 X 里就把它的右边属性并入 X。重复第 2 步直到 X 不再变大为止。闭包有什么用它至少解决三类问题判断 X 是不是超码看 X 是否等于全部属性判断某条依赖是否必然成立左边闭包包含右边就成立求候选码后面要专门讲。来看一个具体的计算过程。设 R(A,B,C,D,E)F{A→B, B→C, C→D, D→E}。求 A初值A {A}。扫描依赖A→B 的左边只有 A已经在 A 里加入 B得到 A {A,B}。再看 B→C左边 B 已在集合里加入 C得到 A {A,B,C}。接着 C→D加入 DD→E加入 E。最终 A {A,B,C,D,E}等于全部属性说明 A 是超码。求 B 呢B→C 加入 CC→D 加入 DD→E 加入 E最终 B {B,C,D,E}没有 A所以 B 单独不是超码。闭包计算是后面所有推导的基础操作这一关必须练到滚瓜烂熟能心算简单题目。3. 码关系中最核心的“身份标识”3.1 超码、候选码、主码、外码别再混了码Key是关系模式里最核心的概念但很多考生对超码、候选码、主码分得不清不楚。我从大到小给你捋一遍。超码能唯一标识一条元组的属性集合。注意它可以是“有冗余”的比如学号、姓名其实学号一个就够唯一标识了加个姓名不影响唯一性但属于冗余的超码。候选码最小的超码也就是去掉任何一个属性都不能再唯一标识元组的属性集合。候选码的关键是“极简”不能多带一个多余属性。主码从候选码里挑一个出来作为主键。一个关系可以有多个候选码但主码只能选一个一般选使用频率高、值稳定的那个。外码一个关系里的属性它引用另一个关系的主码。外码用于把两个表关联起来保证参照完整性。还有一个高频考点是“主属性”和“非主属性”。主属性是指包含在任意一个候选码中的属性非主属性是指不包含在任何候选码中的属性。判断范式时开口第一句通常就是“该关系的候选码是什么主属性有哪些”这一步错了后面全盘皆输。3.2 求候选码的“左右属性法”求候选码是数据库系统工程师考试最常考的基本功考法很固定。核心方法叫“左右属性分类法”把关系 R 中所有属性按它在函数依赖集合 F 中出现的位置分成四类L类只出现在函数依赖左侧的属性。R类只出现在函数依赖右侧的属性。LR类两侧都出现过的属性。N类在 F 中根本没出现过的属性。分类之后按照两条铁律往下推定理L类和N类属性必定出现在每一个候选码中。因为 L 类属性不能被任何其他属性推出来想得到它只能自己出现在码里N 类属性更是跟任何依赖都没关系必须靠码本身带上它。结论R 类属性绝不可能是候选码的一部分因为它完全可以由别的属性推出来放进码里就冗余了。具体操作步骤先找出 L 类和 N 类属性记为集合 M。计算 M 的闭包。如果闭包等于全部属性 U那 M 就是唯一候选码前提是 M 自身没有冗余LN 类属性按定理都是必需项。如果闭包不是 U那就把 LR 类属性逐个或组合加进 M每加一个就算一次闭包直到闭包等于 U并且删掉任何加入的属性后闭包又不再是 U 为止。这样得到的集合就是候选码。这个方法教了十几年可以说屡试不爽。考试时把分类写清楚闭包算一遍整套推导逻辑一分不丢。3.3 候选码实战演练光说方法不够拿两个例子练手。例一设 R(A,B,C,D)F{A→B, B→C, D→B}。先分类。左侧出现的属性是 A、B、D右侧出现的属性是 B、C。那么L类A、D。R类C。LR类B。M {A,D}计算闭包A→B 加入 BD→B 其实也是加入 B但 B 已经在集合里了继续用 B→C 加入 C。最终 (AD) {A,B,C,D} U。所以候选码就是 AD非常简单干净。例二设 R(A,B,C,D,E)F{A→B, C→D, D→E, E→A}。左侧集合是 {A,C,D,E}右侧集合是 {B,D,A,E}。分类结果L类C。R类B。LR类A、D、E。M {C}(C) {C}不等于 U需要尝试从 LR 类中补属性。加 A{A,C}A→B 加入 BC→D 加入DD→E 加入E闭包为 {A,B,C,D,E} U满足条件。但还要验证是不是最小去掉 A 只剩 C 已经试过不行去掉 C 只剩 A 更不行A 单独推到不了 C、D、E。所以 {A,C} 是候选码。加 D{C,D}C→D 直接有 DD→E 加入EE→A 加入AA→B 加入B闭包也是 U。去掉 C 只留 D 显然不行去掉 D 只留 C 更不行所以 {C,D} 也是候选码。加 E{C,E}C→D 加入DD→E 加入EE→A 加入AA→B 加入B同样闭包是 U{C,E} 同样是候选码。这个例子说明两点第一一个关系可以有多个候选码每个都最小且能全覆盖第二判断非主属性时要千万注意只要某个属性出现在任意一个候选码里就是主属性。3.4 码与范式判定的联动求码不是目的它是判断范式和分解模式的桥梁。判断一个关系属于第几范式思路永远固定先找候选码再列主属性和非主属性然后逐个检查是否有部分依赖、传递依赖以及每个函数依赖的左边是否都是超码。比如一个关系候选码是学号、课程号非主属性有学分这时如果存在 课程号→学分那学分对主键就是部分依赖该关系连 2NF 都达不到。很多考生判断范式之前懒得先写码结果后面全靠“感觉”这是最大的失分点。4. 多值依赖表象之下更隐蔽的冗余4.1 多值依赖为什么比函数依赖“难缠”函数依赖描述的是“一个值决定另一个值”多值依赖描述的却是“一组值独立于另一组值”。它没有函数依赖那么直观但造成的冗余反而更隐蔽。看一个经典例子。假设有这样一张表记录学生选修的课程同时记录每个学生的兴趣爱好。学生爱好选课表学号课程爱好把它数据填充出来学号课程爱好S01数学篮球S01英语篮球S01数学唱歌S01英语唱歌S01 选了“数学、英语”两门课同时有“篮球、唱歌”两个爱好。为了把两件事都记下来不得不把所有组合都写一遍生成 2×24 行。这就是典型的笛卡尔积式冗余。这里面没有函数依赖课程和爱好互不决定学号也决定不了课程或爱好的具体值。但这里存在多值依赖学号→→课程学号→→爱好。它的含义是给定一个学号课程取值构成一个集合这个集合不依赖于对应的爱好集合反过来也是。两个属性集合独立地“平行发展”就必然引发组合爆炸。多值依赖的定义可以这样表述在关系 R 中若给定 X 的值Y 的值有一个确定的集合且这个集合不受其他属性取值影响则称 X 多值决定 Y记作 X→→Y。这里必须强调一个重要区分任何一个函数依赖 X→Y都能推出相应的多值依赖 X→→Y因为 X 确定唯一的 Y自然也是一组“只有一个元素”的值。但反过来不成立多值依赖不一定是函数依赖。判断平凡多值依赖的条件有两个要么 Y 是 X 的子集要么 X∪Y 等于全部属性 R。如果这两个条件都不满足就是非平凡多值依赖它才是 4NF 需要处理的对象。4.2 多值依赖的推导规则多值依赖也有类似函数依赖的公理体系考试中偶尔会要求判断多值依赖推导是否成立。核心规则有下面几条补集规则如果 X→→Y那么 X→→R-X-Y。也就是说多值依赖是对称的X 同时多值决定剩余的属性集合。这在做题时很有用题目给出一个多值依赖暗示可能还藏着一个互补的依赖。增广规则如果 X→→Y且 V 是 W 的子集那么 XW→→YV。传递规则如果 X→→YY→→Z那么 X→→Z-Y。复制规则如果 X→Y那么 X→→Y。这些规则不需要死记硬背它本质上描述的是“两组属性是否独立”。理解了这个语义做题时不少选项能直接排除。4.3 第四范式把“独立集合”拆开4NF 的定义关系模式 R 属于 4NF当且仅当对每一个非平凡多值依赖 X→→YX 都是超码。换句话说如果一个表里存在“并行的两组属性”且它们的共同决定因素不是码就必须把它拆开。还用上面那个选课爱好的例子存在学号→→课程、学号→→爱好且学号不是超码这个关系只能是 4NF 以下。解决办法很简单分解成两个表。学生选课表学号课程学生爱好表学号爱好分解后第一个表里只有学号和课程多值依赖已经变成平凡的X∪Y 等于整个属性集第二个表同理。它们各自都达到 4NF数据也从 4 行降到 224 行而且新增一门课程时不需要重复爱好记录。判断 4NF 的题型在考试中一般出现在两张表的比较题里一张表同时包含两类独立属性另一张拆开了。学会用“是否存在非平凡多值依赖”这个标尺去卡基本不会错。5. 规范化层次与实战判定5.1 从1NF到BCNF的核心脉络把 1NF 到 4NF 放到一起看脉络非常清晰1NF所有属性都是原子的不可再分。这是最基本要求连 1NF 都达不到的表不值得讨论。2NF消除非主属性对候选码的部分依赖。也就是说每个非主属性都必须完全依赖于候选码。3NF消除非主属性对候选码的传递依赖。允许存在主属性之间的传递依赖但非主属性不能间接依赖码。BCNF比 3NF 更进一步要求每一个非平凡函数依赖的左边都是超码。这里不再区分主属性还是非主属性任何属性都不能“决定”其他属性除非它能作为超码。4NF在上面基础上再消除非平凡多值依赖。我做一个类比帮助你串联记忆1NF 是“每一格只放一个不能再拆的零件”2NF 是“每样东西都要直接挂在主键名下”3NF 是“副件必须由主键直接领取不能通过中间人代领”BCNF 则是“不管主属性还是非主属性能发号施令的必须是超级码”。这样一条线走下来考场上遇到“这个表属于第几范式”的题就不慌了。5.2 范式判定标准动作碰到“判断范式等级”的题按标准流程走一步不落第一步写出候选码标出所有主属性。 第二步找出所有非主属性。 第三步检查是否存在非主属性对候选码的部分依赖。有则只满足 1NF没有则达到 2NF。 第四步检查是否存在非主属性对候选码的传递依赖。有则只满足 2NF没有则达到 3NF。 第五步检查每个非平凡函数依赖的左边是否都是超码。如果是达到 BCNF。 第六步检查每个非平凡多值依赖的左边是否都是超码。如果是达到 4NF。我前面提到的一个例子很适合走一遍完整流程。设 R(A,B,C,D)F{A→B, BC→D}求该关系最高能达到第几范式。先找候选码。左侧属性是 A、B、C右侧属性是 B、D。分类L类是 A、CR类是 DLR类是 B。M{A,C}闭包计算(AC) {A,B,C,D}U所以候选码只有 AC主属性是 A、C非主属性是 B、D。检查部分依赖A→B 这条依赖中A 是候选码 AC 的真子集非主属性 B 部分依赖于候选码。所以这个关系连 2NF 都不到最高只能算 1NF。进一步思考可以按 BCNF 分解算法处理A→B 违反 BCNF将其拆成 R1(A,B) 和 R2(A,C,D)。在 R1 里候选码是 ABCNF 满足在 R2 里候选码是 AC原依赖 BC→D 中 B 不在 R2 里但如果保留推导出的 AC→D左边是超码也满足 BCNF。最终得到 BCNF 分解。这里需要注意分解时按闭包保留依赖关系不能只保留原始函数依赖集合里的显式项。5.3 工程中要不要“无脑冲BCNF”理论归理论实际工程里并不是范式越高越好。BCNF、4NF 确实能消冗余但代价是查询时频繁 JOIN性能会受影响。我见过一些新手接手一个库就直接把所有表都拆到 4NF结果线上接口慢了两倍最后又不得不做反规范化。实际项目的通用做法是核心交易类数据、变动频繁且一致性要求高的表优先保证 3NF 甚至 BCNF查询密集、更新少、冗余反而能省 JOIN 的场景适当保留部分冗余只要在代码层面维护好一致性即可。数据库系统工程师考试里题目都会指定“按规范化要求设计”所以考场上一律按范式分层回答不要画蛇添足谈性能取舍但在真实系统设计里“规范化是基准反规范化是优化”这个平衡感要心里有数。6. 常见失分点与备考建议6.1 这些坑年年有人踩我批改过不少考前模拟卷发现有几个错误出现的频率非常高。第一个坑闭包计算时“跳跃式”推导跳过了中间依赖。比如 F 里只有 A→B 和 B→C有人直接把 C 加入 A理由是“A 能决定 C”。这在结果上没错但如果题目要求展示推导过程必须在第一次扫描加入 B第二次扫描才能加入 C。严格按算法步骤写既不易错阅卷也给分。第二个坑把 R 类属性放进候选码里。比如 F{A→B, B→C}有人算出 A 是键后又把 C 也塞进去凑成 {A,C}理由是“这样更全面”。这完全违背候选码“最小”的原则。C 是 R 类属性不可能是候选码成员加进去就是冗余。第三个坑判断范式时忽略主键子集。像前面 R(A,B,C,D)F{A→B, BC→D} 的情况不少考生算出候选码 AC 之后愣是看不出 A→B 是部分依赖因为他们的思维停留在“主键是单列”的惯性上。看到复合候选码必须逐个检查依赖左边是不是候选码真子集。第四个坑将多值依赖和函数依赖混为一谈。给出 X→→Y 和一个具体 Y 值有人会直接问“那这个值是谁决定的”这就跑偏了。多值依赖关心的是集合的独立性不是单个值的双向决定关系。判断范式层时函数依赖卡 2NF/3NF/BCNF多值依赖只卡 4NF两条线要分开。第五个坑分解时丢掉依赖。BCNF 分解不仅要消除冗余还要保证分解后的关系能无损连接并保持函数依赖。有些分解方案能满足无损连接但把某些函数依赖“拆没了”这种分解严格来说不是一个好的分解。真题里如果要求“分解到BCNF并保持函数依赖”一定要检查原始 F 中每条依赖在分解后的某个关系里是否仍然成立。6.2 备考顺序和刷题技巧基于我的经验这部分的备考顺序应该是先闭包再候选码再范式判断最后规范化分解。闭包是工具候选码是定位范式判断是应用分解是综合。前面没练熟后面做题必然卡壳。刷题时有一个动作很关键每道题无论简繁都把“写候选码、列主属性/非主属性、逐条检查依赖、得出结论”这个流程完整写在草稿纸上。我见过太多人因为“答案对了”就跳过过程结果换一道稍微变通的题就露馅。规范化的题贵在可验证过程干净比结果漂亮更有价值。最后分享一个我的个人做法考前复习时把 Armstrong 公理、闭包计算法、左右属性分类法、范式判定标准动作这四块内容各自压缩成一句话写在便利贴上贴在笔记本封面。临进考场前看一遍脑海里把整个框架过一遍。这套动作我用在很多届学生身上反馈都很好。数据库规范化理论是那种“一旦理顺就终生不忘”的知识值得你花一个下午彻底弄明白。