
简介这份资源是西南交通大学数据库原理课程第六章「关系数据库设计理论」的作业文档面向正在学习数据库课程的高校学生尤其适合需要完成课后作业或备考复习的同学。文档围绕关系模式RSidSnameCidCnameScoreTid展开涵盖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 → YY → 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→CC→D 也是传递依赖。存在非主属性对候选码的传递依赖所以不满足 3NF。第三步分解到 3NF。按传递依赖链拆开R1(A, B)R2(B, C)R3(C, D)。第四步验证无损连接。R1 和 R2 的公共属性是 BB 是 R2 的候选码所以 R1 和 R2 的无损连接成立。再把结果和 R3 连接公共属性是 CC 是 R3 的候选码无损连接成立。第五步验证保持依赖。R1 投影出 A→BR2 投影出 B→CR3 投影出 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→BB→AA 和 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, labelf{left}-{right}) pos nx.spring_layout(G, seed42) colors [red if n in .join(candidate_keys) else lightblue for n in G.nodes()] nx.draw(G, pos, with_labelsTrue, node_colorcolors, node_size800, font_size12) edge_labels nx.get_edge_attributes(G, label) nx.draw_networkx_edge_labels(G, pos, edge_labelsedge_labels, font_size8) 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。这个技巧帮我省了很多反复推导的时间尤其是依赖集比较大的题目。最后说一个我自己的教训早期做数据库设计时总觉得范式理论是纸上谈兵直到线上出了几次数据不一致的事故才回头把课本翻出来重新学。关系数据库设计理论不是让你背定义而是给你一套判断表结构好坏的标尺。作业里的每道题背后都是一个真实场景的简化版。把候选码求准、把范式判对、把分解验证清楚这套功夫练好了以后设计任何业务表都不会出大问题。希望帮到你。本文还有配套的精品资源点击获取