ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

数据库设计实战:从函数依赖到3NF分解的完整推演

数据库设计实战:从函数依赖到3NF分解的完整推演 简介这份资源是西南交通大学数据库原理课程第六章「关系数据库设计理论」的作业文档面向正在学习数据库课程的高校学生尤其适合需要完成课后作业、复习范式与函数依赖知识点的同学参考。压缩包内共1个docx文件约52KB内容涵盖简答题与设计题两大板块包括关系模式异常原因、数据依赖分类、函数依赖分类、1NF至BCNF的规范化方法等理论要点。设计题以关系模式RSidSnameCidCnameScoreTid为例要求完成反向工程ERM、给出函数依赖、分解为3NF并附有参考答案与分解思路。目前已有481人学习下载读者可借此对照检查自己的ERM画法、函数依赖推导及3NF分解结果理解部分函数依赖与传递函数依赖的消除过程适合作为作业自查与期末复习的辅助材料。1. 一份被低估的数据库设计作业从函数依赖到 3NF 分解的完整推演很多同学拿到西南交通大学数据库原理作业-第6章 关系数据库设计理论这份文档时第一反应是这不就是一份课后答案吗。但真正把里面的设计题从头推一遍就会发现它其实是一道浓缩了关系数据库设计理论核心链路的好题从一个六属性的关系模式出发反向工程出 ERM再推导函数依赖集最后做 3NF 分解。这条链路走通了数据库课程设计里的表结构设计、范式判断、冗余消除基本都能应付。这份资源适合正在学数据库原理、准备课程设计或期末复习的本科生也适合想重新梳理函数依赖和范式这块知识点的开发者。它不是什么高深的东西但胜在题目设计得紧凑一个案例把 1NF 到 BCNF 的判断逻辑全串起来了。2. 关系模式 R 的语义拆解六属性背后的实体与联系2.1 从属性列表还原实体Sid、Cid、Tid 各自代表什么拿到 RSidSnameCidCnameScoreTid第一步不是急着写函数依赖而是先搞清楚这六个属性分别描述了什么。Sid 和 Sname 描述学生Cid 和 Cname 描述课程Tid 描述教师Score 描述学生和课程之间的选课结果。这里有一个容易翻车的地方很多同学看到 Cname 和 Tid 在同一个关系里就默认课程和教师是绑定的但题目明确说了课程与教师间的联系为 1:1意味着每门课只有一位教师每位教师也只教一门课。这个 1:1 的语义直接决定了后面函数依赖的方向。从 ERM 的角度看这里有三个实体学生SidSname、课程CidCname、教师Tid以及两个联系学生与课程之间的 m:n 选课联系带 Score 属性课程与教师之间的 1:1 授课联系。反向工程的关键就是把这些实体和联系从一张宽表里拆出来。2.2 语义约束怎么转成函数依赖1:1、m:n 和唯一性约束的处理题目给了四条语义要求每一条都对应函数依赖的推导方向一名学生只能有一个学号且学号唯一Sid → Sname同时 Sid 是候选键。一门课程只能有一个课程号且课程号唯一Cid → CnameCid 是候选键。课程与教师间的联系为 1:1Cid → Tid 且 Tid → Cid双向决定。学生与课程间的联系为 m:nSid 和 Cid 单独都不能决定 Score必须 (Sid, Cid) 联合才能决定。这里有一个新手常踩的坑把 Cid → Tid 和 Tid → Cid 写成两个独立的依赖就完事了但在判断 BCNF 的时候Tid → Cid 这个方向会导致 Tid 成为决定因素但 Tid 本身不是候选键候选键是 Sid 和 Cid 的组合以及 Tid 和 Cid 的组合这正是 3NF 和 BCNF 之间的分界线。常见做法是先把所有函数依赖列出来再标注哪些是平凡依赖、哪些是非平凡依赖、哪些是完全依赖、哪些是部分依赖、哪些是传递依赖。这份作业的简答题部分已经把分类框架给出来了设计题要做的就是往框架里填具体内容。3. 函数依赖集的推导与最小覆盖从语义到 F 集3.1 完整函数依赖集的列出与分类根据题目语义R 上的函数依赖集 F 可以写成F { Sid → Sname, -- 学号决定姓名 Cid → Cname, -- 课程号决定课程名 Cid → Tid, -- 课程决定授课教师1:1 Tid → Cid, -- 教师决定授课课程1:1 (Sid, Cid) → Score, -- 学生和课程联合决定成绩 (Sid, Cid) → Sname, -- 联合决定学生姓名冗余但成立 (Sid, Cid) → Cname, -- 联合决定课程名冗余但成立 (Tid, Cid) → Cname -- 教师和课程联合决定课程名冗余但成立 }这里面 (Sid, Cid) → Sname 和 (Sid, Cid) → Cname 其实是冗余的因为 Sid → Sname 和 Cid → Cname 已经单独成立了。同理 (Tid, Cid) → Cname 也是冗余的。在求最小覆盖的时候这些冗余依赖需要被去掉。3.2 属性闭包计算与候选键的确定候选键的确定是后续范式判断的基础。用属性闭包的方法来算求 (Sid, Cid) 的闭包(Sid, Cid) {Sid, Sname, Cid, Cname, Tid, Score}覆盖了全部属性所以 (Sid, Cid) 是一个候选键。求 (Tid, Cid) 的闭包(Tid, Cid) {Tid, Cid, Cname, Sname, Score}不对Tid → Cid 已经成立所以 (Tid, Cid) 实际上等价于 Tid 或 Cid 单独。但 Tid 单独能不能决定 Score不能因为 Score 需要 (Sid, Cid) 联合决定。所以 (Tid, Cid) 不是候选键。再检查有没有其他组合能覆盖全部属性。Sid 单独不行缺 Cid 相关属性Cid 单独不行缺 Sid 相关属性Tid 单独不行。所以唯一的候选键就是 (Sid, Cid)。这里有一个容易搞混的点题目说课程与教师间的联系为 1:1Cid → Tid 和 Tid → Cid 同时成立这意味着 Cid 和 Tid 互相决定。但在 R 这个关系模式里Cid 和 Tid 都不是候选键因为它们不能单独决定 Score。候选键只有一个(Sid, Cid)。用 SQL 来验证候选键的思路是这样的-- 假设已经建好表 R验证 (Sid, Cid) 是否唯一决定所有属性 SELECT Sid, Cid, COUNT(DISTINCT Sname) AS sname_cnt, COUNT(DISTINCT Cname) AS cname_cnt, COUNT(DISTINCT Tid) AS tid_cnt, COUNT(DISTINCT Score) AS score_cnt FROM R GROUP BY Sid, Cid HAVING sname_cnt 1 OR cname_cnt 1 OR tid_cnt 1 OR score_cnt 1; -- 如果查询返回空结果说明 (Sid, Cid) 确实能唯一决定所有属性这段 SQL 的逻辑是按 (Sid, Cid) 分组后如果每个分组内 Sname、Cname、Tid、Score 都只有一个不同的值说明 (Sid, Cid) 是候选键。实际做作业的时候不一定有数据库环境但用这个思路来验证自己的推导结论是靠谱的。3.3 最小覆盖的求解步骤最小覆盖Minimal Cover的求解分三步右边单一化、去掉冗余依赖、去掉左边冗余属性。对 F 集处理第一步所有依赖的右边已经是单一属性不需要拆分。第二步检查冗余依赖。Sid → Sname 不能去掉因为去掉后 Sname 无法被其他依赖推出。Cid → Cname 同理。Cid → Tid 和 Tid → Cid 互相不能去掉对方因为去掉任何一个都会丢失 1:1 的语义。 (Sid, Cid) → Score 不能去掉。(Sid, Cid) → Sname 可以去掉因为 Sid → Sname 已经能推出。(Sid, Cid) → Cname 可以去掉因为 Cid → Cname 已经能推出。(Tid, Cid) → Cname 可以去掉因为 Cid → Cname 已经能推出。第三步检查左边冗余属性。(Sid, Cid) → Score 中Sid 单独不能决定 ScoreCid 单独也不能决定 Score所以左边没有冗余属性。最终最小覆盖为F_min { Sid → Sname, Cid → Cname, Cid → Tid, Tid → Cid, (Sid, Cid) → Score }这个最小覆盖是后续 3NF 分解的输入。很多同学在做分解的时候直接凭感觉拆表结果要么丢了依赖要么拆出来的表不满足 3NF问题就出在没有先求最小覆盖。4. 3NF 分解实操投影分解法与依赖保持4.1 为什么选择投影分解法而不是 BCNF 分解题目要求分解成 3NF不是 BCNF。这两个的区别在于3NF 允许主属性对候选键存在传递依赖BCNF 不允许。在 R 这个案例里Tid → Cid 这个依赖中Tid 是主属性因为 Tid 和 Cid 组合可以构成候选键的一部分Cid 也是主属性。如果强行分解到 BCNF可能会丢失 Tid → Cid 这个依赖导致分解不保持依赖。所以题目要求 3NF 是有道理的——在实际工程中保持依赖往往比消除所有冗余更重要。投影分解法的核心思路是对最小覆盖中的每个函数依赖 X → A如果 X 和 A 没有被已有的关系模式覆盖就创建一个新的关系模式 X ∪ {A}。最后如果所有关系模式的属性并集没有覆盖 R 的全部属性再加一个包含剩余属性的关系模式。4.2 逐步分解过程与结果验证按照最小覆盖 F_min 来分解Sid → Sname创建关系模式 S(Sid, Sname)Cid → Cname创建关系模式 C(Cid, Cname)Cid → Tid 和 Tid → Cid创建关系模式 CT(Cid, Tid)(Sid, Cid) → Score创建关系模式 SC(Sid, Cid, Score)检查属性覆盖S 覆盖 {Sid, Sname}C 覆盖 {Cid, Cname}CT 覆盖 {Cid, Tid}SC 覆盖 {Sid, Cid, Score}。全部属性的并集是 {Sid, Sname, Cid, Cname, Tid, Score}正好覆盖 R 的全部属性不需要额外添加关系模式。但这里有一个细节C(Cid, Cname) 和 CT(Cid, Tid) 可以合并成 C(Cid, Cname, Tid)因为 Cid 是两者的公共键。合并后S(Sid, Sname)C(Cid, Cname, Tid)SC(Sid, Cid, Score)这三个关系模式都满足 3NFS 中 Sid 是候选键Sname 完全依赖于 SidC 中 Cid 是候选键Cname 和 Tid 完全依赖于 CidSC 中 (Sid, Cid) 是候选键Score 完全依赖于 (Sid, Cid)不存在部分依赖和传递依赖。用 SQL 建表来验证-- 学生表 CREATE TABLE Student ( Sid CHAR(10) PRIMARY KEY, Sname VARCHAR(50) NOT NULL ); -- 课程表含授课教师 CREATE TABLE Course ( Cid CHAR(10) PRIMARY KEY, Cname VARCHAR(100) NOT NULL, Tid CHAR(10) NOT NULL UNIQUE -- 1:1 约束教师编号唯一 ); -- 选课表 CREATE TABLE Enrollment ( Sid CHAR(10), Cid CHAR(10), Score DECIMAL(5,2), PRIMARY KEY (Sid, Cid), FOREIGN KEY (Sid) REFERENCES Student(Sid), FOREIGN KEY (Cid) REFERENCES Course(Cid) );注意 Course 表中 Tid 加了 UNIQUE 约束这是为了体现 1:1 的语义。如果只是普通字段1:1 的约束就丢了。这个细节在作业答案里不一定写出来但实际建表的时候必须考虑。4.3 分解后的依赖保持性检查分解是否保持依赖需要检查 F_min 中的每个依赖是否都能在分解后的某个关系模式中找到。Sid → Sname 在 S 中Cid → Cname 和 Cid → Tid 在 C 中Tid → Cid 在 C 中因为 Cid 是 C 的主键Tid 是 C 的属性Tid → Cid 在 C 中成立(Sid, Cid) → Score 在 SC 中。所有依赖都保持了分解是依赖保持的。至于无损连接性因为分解中包含了候选键 (Sid, Cid) 所在的模式 SC所以分解也是无损的。这两条性质都满足说明这个 3NF 分解是合格的。5. 避坑指南函数依赖与范式判断中的五个高频翻车点5.1 把 1:1 联系的方向搞反现象在写函数依赖时只写了 Cid → Tid漏掉了 Tid → Cid。原因1:1 联系是双向的很多同学只从课程决定教师这个方向想忘了反过来教师也决定课程。解决遇到 1:1 联系强制自己写出双向依赖然后检查两个方向是否都成立。5.2 候选键漏算或算错现象只找到 (Sid, Cid) 一个候选键但实际可能有多个。原因没有系统性地用属性闭包去验证每个可能的属性组合。解决对每个属性单独求闭包再对两两组合求闭包直到找到所有能覆盖全部属性的最小组合。在 R 这个案例里候选键确实只有 (Sid, Cid)但如果不验证就下结论遇到更复杂的题目就容易出错。5.3 3NF 分解后丢了依赖现象分解出来的关系模式看起来每个都是 3NF但某些函数依赖在分解后无法推导出来了。原因分解时没有按照最小覆盖来拆而是凭直觉拆表。解决先求最小覆盖再对最小覆盖中的每个依赖逐一检查是否被某个关系模式覆盖。如果某个依赖没有被覆盖需要调整分解方案。5.4 把 3NF 和 BCNF 的条件搞混现象判断某个关系模式是不是 BCNF 时把主属性对候选键的传递依赖也算进去了。原因3NF 允许主属性对候选键的传递依赖BCNF 不允许。解决记住 BCNF 的条件更严格——每个决定因素都必须是候选键。在 R 中Tid → Cid 的决定因素 Tid 不是候选键所以 R 不是 BCNF但分解后的 C(Cid, Cname, Tid) 中Cid 是候选键Tid → Cid 的决定因素 Tid 不是候选键所以 C 也不是 BCNF。这就是为什么题目只要求分解到 3NF。5.5 多值依赖和联结依赖的误用现象在只需要考虑函数依赖的题目里硬套 4NF 和 5NF 的条件。原因看到范式两个字就想把所有范式都过一遍。解决先看题目要求分解到第几范式只处理到那一级为止。R 这个案例只要求 3NF多值依赖和联结依赖不需要考虑。简答题里问到了 4NF 和 5NF 的消除条件那是概念题设计题不用管。6. 从作业到实战用 Python 验证函数依赖与自动分解6.1 用 Python 实现属性闭包计算手工推导函数依赖容易出错尤其是属性多的时候。我一般会写一个小脚本来验证闭包和候选键def closure(attrs, fds): 计算属性集 attrs 在函数依赖集 fds 下的闭包 attrs: set of attributes, e.g. {Sid, Cid} fds: list of tuples (left_set, right_set) result set(attrs) changed True while changed: changed False for left, right in fds: if left.issubset(result) and not right.issubset(result): result | right changed True return result # 定义函数依赖集 fds [ ({Sid}, {Sname}), ({Cid}, {Cname}), ({Cid}, {Tid}), ({Tid}, {Cid}), ({Sid, Cid}, {Score}), ] # 验证 (Sid, Cid) 是否是候选键 all_attrs {Sid, Sname, Cid, Cname, Score, Tid} print(closure({Sid, Cid}, fds) all_attrs) # 输出 True # 验证 Sid 单独不是候选键 print(closure({Sid}, fds) all_attrs) # 输出 False这段代码的逻辑很直接从初始属性集出发反复扫描函数依赖集只要某个依赖的左边被当前结果集包含就把右边也加进来直到结果集不再变化。参数 fds 是一个列表每个元素是 (左边属性集, 右边属性集) 的元组。实际用的时候可以把 fds 改成从配置文件读取方便替换不同的题目。6.2 自动检查 3NF 条件的脚本判断一个关系模式是否满足 3NF需要检查每个函数依赖 X → A 是否满足以下任一条件A 属于 X平凡依赖、X 是超键、A 是主属性。用 Python 可以自动化这个检查def is_3nf(attrs, fds, candidate_keys): 判断关系模式是否满足 3NF attrs: 全部属性集 fds: 函数依赖集 candidate_keys: 候选键列表 prime_attrs set() for ck in candidate_keys: prime_attrs | ck for left, right in fds: for a in right: if a in left: continue # 平凡依赖满足 if any(ck.issubset(left) for ck in candidate_keys): continue # left 是超键满足 if a in prime_attrs: continue # a 是主属性满足 return False # 不满足 3NF return True # 验证分解后的 SC(Sid, Cid, Score) attrs_sc {Sid, Cid, Score} fds_sc [({Sid, Cid}, {Score})] cks_sc [{Sid, Cid}] print(is_3nf(attrs_sc, fds_sc, cks_sc)) # 输出 True这个脚本的核心逻辑是对每个函数依赖的右边每个属性逐一检查三个条件。只要有一个属性不满足任何条件整个关系模式就不满足 3NF。参数 candidate_keys 需要提前算好可以用 6.1 的闭包函数来自动搜索所有候选键。6.3 一个我踩过的坑闭包计算中的死循环最早写闭包计算的时候我没有加 changed 标志而是直接遍历 fds 列表每次发现新属性就重新开始遍历。结果遇到循环依赖比如 Cid → Tid 和 Tid → Cid的时候程序会无限循环。后来改成用 changed 标志控制外层循环只有在本轮扫描中有新属性加入时才继续下一轮问题就解决了。从那以后我每次写闭包相关的代码都强制走一遍加 changed 标志 → 测试循环依赖 → 验证终止条件这三步。希望这个小习惯能帮到你。本文还有配套的精品资源点击获取
返回列表