news 2026/10/3 13:19:56

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据库设计实战:从函数依赖到3NF分解的完整推演

简介:这份资源是西南交通大学数据库原理课程第六章「关系数据库设计理论」的作业文档,面向正在学习数据库课程的高校学生,尤其适合需要完成课后作业、复习范式与函数依赖知识点的同学参考。压缩包内共1个docx文件,约52KB,内容涵盖简答题与设计题两大板块,包括关系模式异常原因、数据依赖分类、函数依赖分类、1NF至BCNF的规范化方法等理论要点。设计题以关系模式R(Sid,Sname,Cid,Cname,Score,Tid)为例,要求完成反向工程ERM、给出函数依赖、分解为3NF,并附有参考答案与分解思路。目前已有481人学习下载,读者可借此对照检查自己的ERM画法、函数依赖推导及3NF分解结果,理解部分函数依赖与传递函数依赖的消除过程,适合作为作业自查与期末复习的辅助材料。

1. 一份被低估的数据库设计作业:从函数依赖到 3NF 分解的完整推演

很多同学拿到"西南交通大学数据库原理作业-第6章 关系数据库设计理论"这份文档时,第一反应是"这不就是一份课后答案吗"。但真正把里面的设计题从头推一遍就会发现,它其实是一道浓缩了关系数据库设计理论核心链路的好题:从一个六属性的关系模式出发,反向工程出 ERM,再推导函数依赖集,最后做 3NF 分解。这条链路走通了,数据库课程设计里的表结构设计、范式判断、冗余消除基本都能应付。这份资源适合正在学数据库原理、准备课程设计或期末复习的本科生,也适合想重新梳理函数依赖和范式这块知识点的开发者。它不是什么高深的东西,但胜在题目设计得紧凑,一个案例把 1NF 到 BCNF 的判断逻辑全串起来了。

2. 关系模式 R 的语义拆解:六属性背后的实体与联系

2.1 从属性列表还原实体:Sid、Cid、Tid 各自代表什么

拿到 R(Sid,Sname,Cid,Cname,Score,Tid),第一步不是急着写函数依赖,而是先搞清楚这六个属性分别描述了什么。Sid 和 Sname 描述学生,Cid 和 Cname 描述课程,Tid 描述教师,Score 描述学生和课程之间的选课结果。这里有一个容易翻车的地方:很多同学看到 Cname 和 Tid 在同一个关系里,就默认课程和教师是绑定的,但题目明确说了"课程与教师间的联系为 1:1",意味着每门课只有一位教师,每位教师也只教一门课。这个 1:1 的语义直接决定了后面函数依赖的方向。

从 ERM 的角度看,这里有三个实体:学生(Sid,Sname)、课程(Cid,Cname)、教师(Tid),以及两个联系:学生与课程之间的 m:n 选课联系(带 Score 属性),课程与教师之间的 1:1 授课联系。反向工程的关键就是把这些实体和联系从一张宽表里拆出来。

2.2 语义约束怎么转成函数依赖:1:1、m:n 和唯一性约束的处理

题目给了四条语义要求,每一条都对应函数依赖的推导方向:

  • "一名学生只能有一个学号,且学号唯一":Sid → Sname,同时 Sid 是候选键。
  • "一门课程只能有一个课程号,且课程号唯一":Cid → Cname,Cid 是候选键。
  • "课程与教师间的联系为 1:1":Cid → Tid 且 Tid → Cid,双向决定。
  • "学生与课程间的联系为 m:n":Sid 和 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:1",Cid → 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 单独不能决定 Score,Cid 单独也不能决定 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)

这三个关系模式都满足 3NF:S 中 Sid 是候选键,Sname 完全依赖于 Sid;C 中 Cid 是候选键,Cname 和 Tid 完全依赖于 Cid;SC 中 (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 标志 → 测试循环依赖 → 验证终止条件"这三步。希望这个小习惯能帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 13:18:13

Linux恶意进程检测:从ps/top命令深入进程行为分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 13:17:10

Python实现SfM三维重建:从特征提取到稀疏点云生成

简介:基于 Python 的三维重建算法 Structure from Motion(Sfm)实现代码,是一份面向高校计算机相关专业学生的课程设计与期末大作业源码包。内容聚焦 Sfm 三维重建核心流程,难度适中,源码均经过本地编译验证…

作者头像 李华
网站建设 2026/10/3 13:16:53

Python监听海康威视报警:HCNetSDK与ISAPI实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 13:16:19

半导体MFC质量流量控制器全解析:原理、选型、校准与故障排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 13:15:53

DRV8818+PIC32MZ工业级步进电机电流闭环控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 13:14:54

精益智能工厂三年规划:从OEE基线到AI排产的落地路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华