数据库系统の规范化理论:怎么设计一张"好"的表?
你有没有见过这样一张表:学生的学号、姓名、院系编号、院系名称、院系地址,全部塞在同一张表里?
看起来"什么都有",用起来却是灾难——新成立的院系没有学生就没法录入、删除最后一个学生会连带丢掉整个院系的信息、改一次院系地址要改几十行数据……
这些问题的根源是同一个:关系模式设计得不好。而规范化理论,就是一套科学的"表格设计方法论"——告诉你什么样的表是"好"的,怎么把"坏"的表拆成"好"的表。
这篇文章把函数依赖、Armstrong公理、属性闭包、五大范式、模式分解全部讲透。期末考试必考的规范化大题,这一篇就够了。
本文目录
问题的提出:为什么需要规范化?
函数依赖:规范化的理论基石
Armstrong公理与属性闭包
最小函数依赖集
范式:从1NF到BCNF
模式分解:怎么把"坏表"拆成"好表"
一、问题的提出:为什么需要规范化?
1.1 一个"坏"的关系模式
假设我们设计这样一张表来管理学生信息:
StudentDept(Sno, Sname, Ssex, Dno, Dname, Daddr)
Sno | Sname | Ssex | Dno | Dname | Daddr |
|---|---|---|---|---|---|
001 | 张三 | 男 | CS | 计算机系 | 1号楼 |
002 | 李四 | 女 | CS | 计算机系 | 1号楼 |
003 | 王五 | 男 | MA | 数学系 | 2号楼 |
看起来什么问题都有——每个学生的院系信息都重复了。但这不只是"浪费空间"的问题,还会导致四种严重的操作异常。
1.2 四种操作异常
数据冗余——计算机系的名称和地址被重复存储了两次(张三和李四各存一份)。如果有100个计算机系的学生,"计算机系, 1号楼"就要重复100次。
插入异常——假设学校新成立了一个"人工智能学院",但还没有招到学生。因为Sno是主码不能为空,所以我们无法把新院系的信息插入到表中——该插入的数据插不进去。
删除异常——假设数学系只有一个学生王五。如果王五毕业了,我们删除王五的记录,连带着数学系的全部信息也丢失了——不该丢的数据被一起删了。
修改异常——计算机系搬到了3号楼。我们需要修改所有计算机系学生的Daddr字段(张三和李四都要改)。如果漏改了某一行,数据就不一致了。
1.3 异常的根源
这四种异常的根源是同一个:关系模式中存在不合适的数据依赖。
具体来说:Sno能决定Sname(每个学号对应唯一的姓名),Dno能决定Dname和Daddr(每个院系有唯一的名称和地址)。但我们把学生信息和院系信息放在了同一张表里,导致了院系信息对Sno的"间接依赖"——这就是问题的核心。
解决思路:把一个大关系分解成多个小关系(模式分解),让每个关系只描述一个"主题"。
二、函数依赖:规范化的理论基石
要科学地设计关系模式,首先需要一种工具来描述属性之间的依赖关系——这就是函数依赖(Functional Dependency, FD)。
2.1 函数依赖的定义
X→Y:在关系R中,如果X的值确定了,Y的值就唯一确定了,则称X函数决定Y,或Y函数依赖于X。
X称为决定因素(Determinant),Y称为被决定因素。
比如在学生表中:Sno→Sname(学号确定姓名),Dno→Dname(院系编号确定院系名称)。
注意:函数依赖是语义层面的——它取决于现实世界中数据之间的实际关系,不是看某个具体实例(数据表)是否满足,而是看所有可能的合法数据是否都满足。
2.2 函数依赖的分类
函数依赖的三种类型:完全依赖、部分依赖、传递依赖
平凡函数依赖——Y是X的子集(如AB→A)。这种依赖总是成立,没有实际意义。
非平凡函数依赖——Y不是X的子集(如Sno→Sname)。这是我们关心的有意义的依赖。
完全函数依赖——Y依赖于X,且不依赖于X的任何真子集。记为X →ᶠ Y。比如(Sno, Cno) → Grade:成绩由学号和课程号共同决定,缺一不可。
部分函数依赖——Y依赖于X,但Y实际上只依赖于X的某个真子集。记为X →ₚ Y。比如(Sno, Cno) → Sname:姓名只依赖于Sno(学号),不依赖于Cno(课程号),所以Sname对复合码(Sno, Cno)是部分依赖。
传递函数依赖——X→Y,Y→Z,且Y不包含于X,Y不决定X。比如Sno→Dno,Dno→Daddr,则Daddr传递依赖于Sno。院系地址不是由学号直接决定的,而是通过院系编号"传递"过来的。
2.3 函数依赖与码的关系
候选码——能函数决定全部属性的最小属性集。"最小"意味着去掉任何一个属性就不能决定全部属性了。
主属性——包含在任一候选码中的属性。
非主属性——不包含在任何候选码中的属性。
全码——所有属性组合起来才是候选码(极端情况)。
三、Armstrong公理与属性闭包
3.1 Armstrong公理
有了函数依赖集F,我们想知道F能推导出哪些新的函数依赖。Armstrong公理提供了三条基本推理规则:
Armstrong公理与属性闭包算法
自反律(Reflexivity):若Y⊆X,则X→Y。意思是:如果Y是X的子集,那么X当然能决定Y。(比如学号+姓名当然能决定姓名——废话。)
增广律(Augmentation):若X→Y,则XZ→YZ。意思是:如果X能决定Y,那么在两边都加上相同的属性集Z,依赖仍然成立。
传递律(Transitivity):若X→Y,Y→Z,则X→Z。意思是:依赖关系可以传递。
由这三条公理可以推导出三条常用规则:
合并律:若X→Y且X→Z,则X→YZ。(左边相同,右边可以合并。)
分解律:若X→YZ,则X→Y且X→Z。(右边可以拆开。)
伪传递律:若X→Y且WY→Z,则WX→Z。(把X→Y代入WY→Z中的Y。)
Armstrong公理是完备的——F能推导出的所有函数依赖,都可以用这三条公理推出来。
3.2 属性闭包
X⁺(X关于F的闭包)——从X出发,利用F中的函数依赖,能函数决定的所有属性的集合。
求闭包的算法:
令X⁺ = X(初始值)
反复扫描F中的每个FD:如果左边⊆X⁺,就把右边加入X⁺
直到X⁺不再增大为止
例题:已知F = {A→B, B→C, CD→E},求A⁺。
初始:A⁺ = {A}
应用A→B:A⁺ = {A, B}
应用B→C:A⁺ = {A, B, C}
应用CD→E:C⊆A⁺但D不在A⁺中,不满足,跳过
没有新的FD可以应用了,最终A⁺ = {A, B, C}
属性闭包的三大用途:
判断候选码:如果X⁺包含了关系的全部属性(X⁺ = U),则X是超码。再检查X的每个真子集的闭包是否≠U,如果都不等于U,则X是候选码(最小性)。
判断FD是否成立:要判断X→Y是否被F所蕴含,只需检查Y是否⊆X⁺。如果是,则X→Y成立。
求最小覆盖:属性闭包是求最小函数依赖集的关键工具。
四、最小函数依赖集(最小覆盖)
4.1 定义
最小覆盖Fmin是与原函数依赖集F等价的最简形式,满足三个条件:
每个FD的右边都是单属性
没有冗余的FD——去掉任何一个FD后,闭包会改变(不等价)
每个FD的左边没有冗余属性——去掉左边任何一个属性后,闭包会改变
4.2 求最小覆盖的步骤
第一步:拆分右边——把每个FD的右边拆成单属性。比如AB→CD拆为AB→C和AB→D。
第二步:消除冗余FD——逐个尝试去掉每个FD,检查去掉后剩下的FD集是否仍然能推出被去掉的FD(用属性闭包判断)。如果能推出,说明这个FD是冗余的,可以去掉。
第三步:消除左边冗余属性——对于左边有多个属性的FD(如AB→C),逐个检查去掉某个属性后(如B→C或A→C),用剩余FD集的属性闭包是否仍能推出C。如果能,则该属性是冗余的。
4.3 注意
最小覆盖不唯一——取决于处理FD的顺序。但任何最小覆盖都与原F等价(能推出相同的函数依赖集)。
五、范式:从1NF到BCNF
范式(Normal Form)是衡量关系模式"好坏"的标准。范式越高,数据冗余越少。
范式层级:1NF ⊃ 2NF ⊃ 3NF ⊃ BCNF
5.1 第一范式(1NF)
要求:每个分量都是不可再分的基本数据项(原子性)。
这是关系数据库的最低要求——不满足1NF的连"关系"都算不上。
反例:如果"地址"属性包含了省、市、街道,没有拆分成独立字段,就不满足1NF。
5.2 第二范式(2NF)
要求:1NF + 消除非主属性对码的部分函数依赖。
含义:每个非主属性必须完全依赖于候选码(不能只依赖码的一部分)。
问题场景:关系SC(Sno, Cno, Sname, Grade),码是(Sno, Cno)。Sname只依赖于Sno(不依赖Cno),所以Sname对码(Sno, Cno)是部分依赖。
解决:分解为两个关系:
Student(Sno, Sname)——学号决定姓名
Score(Sno, Cno, Grade)——学号+课程号决定成绩
分解后,每个非主属性都完全依赖于码,满足2NF。
快速判断:如果候选码是单属性(只有一个属性),则不存在部分依赖,自动满足2NF。
5.3 第三范式(3NF)
要求:2NF + 消除非主属性对码的传递函数依赖。
含义:非主属性不能通过另一个非主属性"传递"依赖于码。
问题场景:关系Student(Sno, Sname, Dno, Daddr),码是Sno。Sno→Dno(学号决定院系编号),Dno→Daddr(院系编号决定地址)。Daddr传递依赖于Sno。
解决:分解为两个关系:
Student(Sno, Sname, Dno)——学生基本信息
Dept(Dno, Daddr)——院系信息
分解后消除了传递依赖,满足3NF。
5.4 BC范式(BCNF)
要求:每个决定因素都是候选码。
BCNF比3NF更严格。3NF只关注非主属性不能传递/部分依赖于码,但BCNF关注所有属性——包括主属性。
3NF和BCNF的区别:
3NF允许的情况:非主属性不传递依赖于码,但主属性可能部分依赖或传递依赖于码。
BCNF消除了这种情况:每个决定因素都必须是候选码——无论是主属性还是非主属性。
满足BCNF一定满足3NF,反之不然。
BCNF的例子:关系STJ(S, T, J),S是学生,T是教师,J是课程。FD:T→J(每个教师只教一门课),(S,J)→T(每个学生每门课只有一个教师),(S,T)→J(每个学生跟每个教师只学一门课)。候选码是(S,J)和(S,T)。T→J中T不是候选码,所以不满足BCNF。
5.5 第四范式(4NF)
4NF引入了多值依赖(MVD)的概念:X→→Y。实际考试中较少涉及,这里简单提一下:4NF要求消除非平凡且非函数依赖的多值依赖。
5.6 范式之间的关系
1NF ⊃ 2NF ⊃ 3NF ⊃ BCNF ⊃ 4NF——每个更高的范式都是更低范式的子集。满足高阶范式一定满足低阶范式,反之不然。
范式越高,数据冗余越少,异常越少。但代价是:查询时可能需要更多的连接(JOIN)操作,因为数据分散在更多的表中。
实际设计中,通常达到3NF或BCNF即可。更高的范式(4NF、5NF)在理论研究中更有意义。
六、模式分解:怎么把"坏表"拆成"好表"
规范化过程:从1NF逐步分解到3NF
6.1 分解的目标
把低范式的关系模式分解为高范式的多个关系模式,减少数据冗余和操作异常。
6.2 无损连接性(Lossless Join)
定义:分解后的关系做自然连接,能完全恢复原关系(不多不少)。
为什么必须无损?如果分解不是无损的,做自然连接后会产生原表中不存在的"伪元组"——这就丢失了信息。
判定方法:
二分分解定理(最常用):R分解为R1和R2,如果满足以下任一条件,则是无损分解:
R1 ∩ R2 → R1 - R2(交集能决定R1独有的属性)
R1 ∩ R2 → R2 - R1(交集能决定R2独有的属性)
表格法(Chase算法):对于分解为多个关系的情况,构造一个初始表,反复应用FD修改表中的值,直到某一行全为a(表示无损)或无法再修改(表示有损)。
6.3 保持函数依赖
定义:分解后,原F中的每个FD都可以在某个子模式上检查。
为什么需要保持?如果不保持,有些约束无法在单个表上验证——需要跨表检查,代价很高。
判定方法:检查每个FD的左右属性是否都包含在某个子模式中。
6.4 分解算法
3NF分解——保证无损连接且保持函数依赖。具体步骤:
求F的最小覆盖Fmin
对Fmin中的每个FD X→Y,生成一个子模式XY
如果某个子模式不包含任何候选码,加入一个包含候选码的子模式
BCNF分解——保证无损连接(但不一定保持函数依赖)。具体步骤:
检查当前关系是否满足BCNF
如果不满足,找到违反BCNF的FD X→Y(X不是候选码)
将R分解为R1(XY)和R2(R-Y)
递归检查R1和R2,直到所有子关系都满足BCNF
6.5 分解的权衡
目标 | 3NF分解 | BCNF分解 |
|---|---|---|
无损连接 | ✓ 保证 | ✓ 保证 |
保持函数依赖 | ✓ 保证 | ✗ 不保证 |
消除异常程度 | 消除非主属性异常 | 消除所有属性异常 |
选择原则:如果要求保持函数依赖(便于约束检查),选3NF分解。如果要求最大程度消除异常,选BCNF分解。实际中,大多数设计达到3NF就够了——3NF已经消除了绝大部分冗余和异常。
总结
规范化理论的核心可以用一条主线串起来:
函数依赖→ 描述属性之间的决定关系,是理论基础。部分依赖和传递依赖→ 导致数据冗余和操作异常的"罪魁祸首"。Armstrong公理→ 从已知FD推导新FD的推理工具。属性闭包→ 判断候选码、验证FD、求最小覆盖的万能工具。范式→ 从1NF到BCNF,每一级消除一种"坏"依赖。模式分解→ 把坏表拆成好表,保证无损连接和(尽量)保持函数依赖。
一句话总结:规范化的本质就是"一事一表"——让每张表只描述一个主题,属性之间有清晰的依赖关系,不冗余、不矛盾、不异常。