☰
关系数据库设计理论:函数依赖、候选码与范式判定作业实战
2026/10/3 7:57:31 网站建设 项目流程

简介:这份资源是西南交通大学数据库原理课程第六章「关系数据库设计理论」的作业文档,面向正在学习数据库课程的高校学生,尤其适合需要完成课后作业或备考复习的同学。文档围绕关系模式R(Sid,Sname,Cid,Cname,Score,Tid)展开,涵盖ERM反向工程、函数依赖推导以及将关系模式分解为3NF等核心题型,并附有简答题与学习体会。压缩包内共1个docx文件,约52KB,内容以题目与参考答案为主,结构清晰,便于对照理解。目前已有481人学习下载,说明该资料在同类课程中具有一定参考价值。通过这份作业,读者可以梳理函数依赖、部分依赖、传递依赖与范式判定等易错点,掌握从语义描述到3NF分解的完整思路,也可作为期末复习时查漏补缺的练习材料。

1. 关系数据库设计理论:从函数依赖到范式,一份作业背后的工程思维

很多人第一次接触“关系数据库设计理论”是在数据库原理课上,老师丢过来一份《西南交通大学数据库原理作业-第6章 关系数据库设计理论.docx》,里面全是函数依赖、候选码、范式判断。你可能会想:这玩意儿除了考试还能干嘛?我做了几年后端开发,踩过最惨的一次坑就是早期设计订单表时没做规范化,用户地址字段冗余存储,结果用户改一次地址要更新几十万行,数据库死锁频发。后来回头翻课本才发现,关系数据库设计理论早就把答案写好了——函数依赖告诉你哪些字段该拆,范式告诉你拆到什么程度算合理。这份作业的核心不是让你背定义,而是训练一种能力:拿到一个业务需求,能推导出合理的表结构,并且能判断当前设计是否存在插入异常、更新异常和删除异常。适合正在做数据库课程设计的学生,也适合工作两三年、想补上数据库设计基本功的开发者。下面我按“理论怎么立住 → 作业怎么动手做 → 坑在哪 → 怎么验证”的顺序,把这份作业背后的东西讲透。

2. 函数依赖与候选码:作业里最常考的两类推导题怎么做

2.1 函数依赖的三种类型与判定方法

函数依赖是关系数据库设计理论的基石。简单说,如果知道一个属性的值就能唯一确定另一个属性的值,就称前者函数决定后者,记作 X → Y。作业里常见的题型是给一个关系模式 R(A, B, C, D, E) 和一组函数依赖 F,让你判断某个依赖是否成立、求候选码、判断范式等级。

先分清三种依赖:

  • 完全函数依赖:X → Y,且 X 的任何真子集都不能决定 Y。比如 (学号, 课程号) → 成绩,单独的学号或课程号都决定不了成绩。
  • 部分函数依赖:X → Y,但 X 的某个真子集也能决定 Y。比如 (学号, 课程号) → 姓名,其实学号 alone 就能决定姓名,这就是部分依赖,是 2NF 要消除的对象。
  • 传递函数依赖:X → Y,Y → Z,且 Y 不决定 X,则 X → Z 是传递依赖。比如 学号 → 系名,系名 → 系主任,那么 学号 → 系主任 就是传递依赖,是 3NF 要消除的对象。

作业里判断依赖类型时,我一般会先把所有属性列出来,然后逐个分析每个依赖的左部能不能再缩小。这里有个血泪经验:不要凭感觉判断“部分依赖”,一定要把左部的所有真子集都列出来验证一遍。很多同学翻车就翻在漏了某个真子集也能决定右部。

2.2 用闭包算法求候选码:手算步骤与代码验证

候选码是能唯一标识元组且不含多余属性的属性集。作业里几乎每道大题都会让你求候选码。手算方法是:先找只在依赖左部出现的属性(一定在候选码里),再找只在右部出现的属性(一定不在候选码里),然后对剩余属性做组合,求闭包看是否等于全部属性集。

闭包算法用代码实现更不容易出错。下面是我用 Python 写的一个求属性集闭包的小工具,作业里验证答案很方便:

def closure(attrs, fds): """ attrs: 初始属性集合,如 {'A', 'B'} fds: 函数依赖列表,每个元素是 (左部集合, 右部集合) 返回 attrs 在 fds 下的闭包 """ result = set(attrs) changed = True while changed: changed = False for left, right in fds: # 如果左部是 result 的子集,则右部可以加入 result if left.issubset(result) and not right.issubset(result): result |= right changed = True return result # 示例:R(A,B,C,D,E),F = {A->BC, CD->E, B->D, E->A} fds = [ ({'A'}, {'B', 'C'}), ({'C', 'D'}, {'E'}), ({'B'}, {'D'}), ({'E'}, {'A'}), ] all_attrs = {'A', 'B', 'C', 'D', 'E'} print(closure({'A'}, fds)) # 输出 {'A','B','C','D','E'},说明 A 是候选码 print(closure({'E'}, fds)) # 输出 {'A','B','C','D','E'},说明 E 也是候选码 print(closure({'C', 'D'}, fds)) # 输出 {'A','B','C','D','E'},CD 也是候选码

这段代码的逻辑很直接:从初始属性集出发,反复扫描所有函数依赖,只要某个依赖的左部已经被当前结果集包含,就把右部加进来,直到结果集不再变化。参数说明:attrs是你要测试的属性组合,fds是题目给的依赖集,每个依赖用两个集合表示左部和右部。运行后如果闭包等于全部属性集,这个组合就是候选码(还需要验证最小性,即去掉任何一个属性后闭包不再等于全部属性)。

作业里常见的一个坑是:候选码可能有多个,比如上面例子中 A、E、CD 都是候选码。很多同学只找到一个就停了,结果后面判断范式等级时用错了码,整道题崩盘。我一般会先把所有候选码都求出来,再选一个作为主码继续做后续分析。

3. 范式判定与分解:从 1NF 到 BCNF 的作业实操路径

3.1 四个范式的判定条件与常见误判

范式是关系数据库设计理论里最容易混淆的部分。作业里通常要求你判断一个关系模式属于第几范式,或者把它分解到 3NF 或 BCNF。先把判定条件理清楚:

范式判定条件消除的问题
1NF每个属性都是原子的,不可再分表中表
2NF满足 1NF,且非主属性完全依赖于候选码部分函数依赖
3NF满足 2NF,且非主属性不传递依赖于候选码传递函数依赖
BCNF每个决定因素都包含候选码主属性对码的部分和传递依赖

判定顺序很重要:先找候选码,再区分主属性和非主属性,然后逐个检查依赖类型。作业里最常见的误判是把 3NF 和 BCNF 搞混。区别在于:3NF 允许主属性传递依赖于候选码,BCNF 不允许。举个例子,关系模式 STJ(学生, 教师, 课程),依赖是 (学生, 课程) → 教师,教师 → 课程。候选码是 (学生, 课程) 和 (学生, 教师)。教师 → 课程 中教师是决定因素但不包含候选码,所以不满足 BCNF,但满足 3NF,因为课程是主属性,不存在非主属性传递依赖。

我一般会按这个顺序检查:先确认 1NF(看有没有复合属性),再找所有候选码,再标出主属性,然后检查每个函数依赖的左部是否包含候选码。如果所有依赖左部都包含候选码,就是 BCNF;如果只有非主属性不传递依赖,就是 3NF。

3.2 保持依赖和无损连接的分解步骤

作业里另一类大题是:给定一个不满足 3NF 或 BCNF 的关系模式,要求你分解它,并且验证分解是否保持函数依赖、是否无损连接。这是最容易翻车的部分,因为分解方案不唯一,但验证方法必须严格。

保持依赖的验证:把分解后的每个子模式的函数依赖投影出来,求并集,看是否等价于原依赖集。无损连接的验证:用 Chase 算法或者判断分解后的公共属性是否是某个子模式的候选码。

下面是一个分解的实操步骤,以 R(A, B, C, D) 和 F = {A→B, B→C, C→D} 为例:

第一步:求候选码。A 的闭包是 {A,B,C,D},所以 A 是唯一候选码。

第二步:判断范式。A→B 是直接依赖,B→C 是传递依赖(A→B→C),C→D 也是传递依赖。存在非主属性对候选码的传递依赖,所以不满足 3NF。

第三步:分解到 3NF。按传递依赖链拆开:R1(A, B),R2(B, C),R3(C, D)。

第四步:验证无损连接。R1 和 R2 的公共属性是 B,B 是 R2 的候选码,所以 R1 和 R2 的无损连接成立。再把结果和 R3 连接,公共属性是 C,C 是 R3 的候选码,无损连接成立。

第五步:验证保持依赖。R1 投影出 A→B,R2 投影出 B→C,R3 投影出 C→D,并集等于原依赖集,保持依赖成立。

注意:分解到 BCNF 时可能丢失函数依赖。比如上面的例子如果分解成 R1(A, B) 和 R2(A, C, D),虽然满足 BCNF,但 B→C 和 C→D 丢失了。作业里如果要求同时保持依赖和无损连接,通常只能分解到 3NF。

3.3 用 SQL 建表验证范式分解结果

理论推导完之后,我习惯用 SQL 把分解后的表建出来,插入几条测试数据,看看是否存在插入异常或更新异常。这一步能帮你直观感受范式分解的意义。

-- 未规范化的原始表:存在传递依赖和冗余 CREATE TABLE orders_raw ( order_id INT, customer_id INT, customer_name VARCHAR(50), customer_city VARCHAR(50), product_id INT, product_name VARCHAR(50), quantity INT, PRIMARY KEY (order_id, product_id) ); -- 问题:customer_name 和 customer_city 完全依赖于 customer_id, -- 但 customer_id 不是候选码的一部分,存在部分依赖和传递依赖 -- 分解到 3NF 后的表结构 CREATE TABLE customers ( customer_id INT PRIMARY KEY, customer_name VARCHAR(50), customer_city VARCHAR(50) ); CREATE TABLE products ( product_id INT PRIMARY KEY, product_name VARCHAR(50) ); CREATE TABLE orders ( order_id INT, customer_id INT, product_id INT, quantity INT, PRIMARY KEY (order_id, product_id), FOREIGN KEY (customer_id) REFERENCES customers(customer_id), FOREIGN KEY (product_id) REFERENCES products(product_id) );

建完表之后,你可以试着插入一条订单数据,然后修改客户所在城市。在原始表里你需要更新所有包含该客户的订单行,在分解后的表里只需要更新 customers 表的一行。这就是范式分解的实际价值。作业里如果要求你说明分解的好处,直接拿这个例子写就行。

4. 作业里最容易翻车的五个坑:从候选码漏解到范式误判

4.1 候选码求漏:只找到一个就收手

现象:题目给的依赖集里存在多个候选码,你只找到了一个,后续范式判断全部基于错误的码。

原因:候选码可能由不同属性组合构成,尤其是当依赖集里存在循环依赖时(如 A→B,B→A),A 和 B 都是候选码。很多同学只从“只在左部出现的属性”开始找,忽略了右部属性也可能参与构成候选码。

解决:把所有属性分成四类——只在左部出现(必在候选码)、只在右部出现(必不在候选码)、左右都出现(可能参与)、左右都不出现(必在候选码)。然后对“左右都出现”的属性做组合,逐个求闭包验证。组合数不多时手算,多了就用上面给的 Python 闭包函数跑一遍。

4.2 部分依赖和传递依赖混淆

现象:判断 2NF 和 3NF 时把部分依赖当成传递依赖,或者反过来,导致范式等级判断错误。

原因:部分依赖是“候选码的真子集决定非主属性”,传递依赖是“非主属性决定非主属性”。两者的结构不同,但初学者容易看到“间接决定”就归为传递依赖。

解决:先确认候选码,然后对每个非主属性,看它是被候选码的哪个部分决定的。如果候选码是真子集就能决定它,是部分依赖;如果它被另一个非主属性决定,是传递依赖。画一个依赖图会更清楚:候选码在顶层,直接依赖的在第二层,传递依赖的在第三层。

4.3 分解时丢失函数依赖

现象:把关系模式分解到 BCNF 后,验证发现某些函数依赖在两个子模式里都找不到。

原因:BCNF 分解算法是迭代消除非候选码决定因素,每次分解可能把依赖的左右部拆到不同子模式里。比如 R(A, B, C) 和 F = {AB→C, C→A},候选码是 AB 和 BC。C→A 中 C 不包含候选码,违反 BCNF。分解成 R1(C, A) 和 R2(B, C),AB→C 这个依赖就丢失了。

解决:如果题目要求保持依赖,就不要强行分解到 BCNF,分解到 3NF 即可。3NF 的合成算法能保证保持依赖和无损连接。如果题目只要求无损连接,BCNF 分解可以接受依赖丢失,但要在答案里注明哪些依赖丢失了。

4.4 无损连接验证时用错公共属性

现象:判断两个子模式的无损连接时,看到公共属性就认为无损,没有验证公共属性是否是某个子模式的候选码。

原因:无损连接的判定条件是:分解后的两个子模式的公共属性必须是其中一个子模式的候选码。仅仅有公共属性不够,公共属性必须能唯一标识该子模式的所有属性。

解决:对每对子模式,求公共属性集,然后求这个公共属性集在对应子模式上的闭包,看是否等于该子模式的全部属性。如果是,无损连接成立。多个子模式时用 Chase 算法逐步验证。

4.5 把范式等级和实际性能划等号

现象:认为范式越高越好,把所有表都拆到 BCNF,结果查询时需要大量 JOIN,性能反而下降。

原因:范式分解消除冗余的同时也增加了连接操作。在实际工程中,查询性能和数据一致性需要权衡。作业里判断范式等级是一回事,实际建表是另一回事。

解决:作业里按题目要求做范式判断和分解,但心里要清楚:实际项目中,读多写少的场景可以适当反范式,用冗余换查询性能;写多读少的场景优先保证范式,减少更新异常。我一般会在 3NF 的基础上,对高频查询涉及的少量字段做冗余,而不是盲目追求 BCNF。

5. 用依赖图快速判断范式等级:一个省时间的技巧

做作业时最耗时的不是计算,而是反复检查有没有漏掉某个依赖。我后来养成一个习惯:先把所有函数依赖画成有向图,节点是属性,边是依赖关系。然后按下面的规则快速判断:

  • 如果图中有任何节点的入边来自非候选码属性,检查是否存在传递依赖。
  • 如果某个依赖的左部是候选码的真子集,标记为部分依赖。
  • 如果所有依赖的左部都包含候选码,直接判定 BCNF。

这个技巧在考试和作业里能省不少时间。下面是一个用 Python 画依赖图并自动标注候选码的脚本,你可以直接套用到作业题目上:

import networkx as nx import matplotlib.pyplot as plt def draw_fd_graph(attrs, fds, candidate_keys): """ attrs: 全部属性列表 fds: 函数依赖列表,每个元素是 (左部字符串, 右部字符串) candidate_keys: 候选码列表,每个元素是属性字符串 """ G = nx.DiGraph() G.add_nodes_from(attrs) for left, right in fds: for l in left: for r in right: G.add_edge(l, r, label=f"{left}->{right}") pos = nx.spring_layout(G, seed=42) colors = ['red' if n in ''.join(candidate_keys) else 'lightblue' for n in G.nodes()] nx.draw(G, pos, with_labels=True, node_color=colors, node_size=800, font_size=12) edge_labels = nx.get_edge_attributes(G, 'label') nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=8) plt.title("函数依赖图(红色节点为候选码属性)") plt.show() # 示例 attrs = ['A', 'B', 'C', 'D', 'E'] fds = [('A', 'BC'), ('CD', 'E'), ('B', 'D'), ('E', 'A')] candidate_keys = ['A', 'E', 'CD'] draw_fd_graph(attrs, fds, candidate_keys)

这段代码用 networkx 构建有向图,节点颜色区分候选码属性和非候选码属性,边标签显示具体的函数依赖。运行后你能一眼看出哪些属性是“源头”(只有出边没有入边),哪些是“终点”(只有入边没有出边)。候选码属性通常分布在图的强连通分量附近。参数说明:attrs是属性列表,fds是依赖对,candidate_keys是你已经求出的候选码列表。如果还没求候选码,可以先跑前面的闭包函数。

我一般会先用闭包函数求出所有候选码,再画图验证。图里如果发现某个非候选码属性有出边指向另一个非候选码属性,那基本可以确定存在传递依赖,范式等级不会超过 3NF。这个技巧帮我省了很多反复推导的时间,尤其是依赖集比较大的题目。

最后说一个我自己的教训:早期做数据库设计时总觉得范式理论是纸上谈兵,直到线上出了几次数据不一致的事故,才回头把课本翻出来重新学。关系数据库设计理论不是让你背定义,而是给你一套判断表结构好坏的标尺。作业里的每道题,背后都是一个真实场景的简化版。把候选码求准、把范式判对、把分解验证清楚,这套功夫练好了,以后设计任何业务表都不会出大问题。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询