关系型数据库设计理论
约 4140 字大约 14 分钟
2025-10-04
重要的术语
关系模型是一种基于表的数据模型,以下为关系学生信息,该表有诸多不足之处,本文研究如何改进它:

下面是一些重要术语:
关系(relation):一个规范的、具有严格定义的二维表。它由元组的集合构成,每个元组对应一个实体。在大多数语境下,可以理解为“表(table)”。
表(table):由多个属性 + 多个元组所表示的各个实例组成。是物理体现。
属性(arrtibute):列的名字,上图有学号、姓名、班级、兴趣爱好、班主任、课程、授课主任、分数。
域(domain):一组具有相同数据类型的值的集合。它定义了列的合法取值范围。如:性别 的域可能是 {'男', '女'}, 年龄 的域可能是 1 到 150 的整数。
模式(schema):对数据库结构的描述,它不包含实际的数据。它包括关系名、属性名、域、完整性约束等。例如:学生(学号,姓名,专业) 就是一个简单的模式。它描述了结构,但不包含具体的学生数据。
依赖(relation): 列属性之间存在的某种联系
元组(tuple):每一行叫做一个元组,如第一行(1301,小明,13班,篮球,王老师,数学,张数,80)
键/超键(key/superkey):能唯一标识关系中一个元组的一个或多个属性的集合。
候选键(candidate key):若关系中的某一属性或属性组的值能唯一标识一个元组,则称该属性或属性组为候选键。候选键都具备键的特征,都有资格成为主键。
主键(primary key):从候选键中被选定的,用来唯一标识元组的那个键。值必须唯一且非空。
主属性(prime attribute):所有候选键所包含的属性都是主属性。
外键(foreign key):如果某一个关系A中的一个属性是另一个关系B的键,则该属性在A中称为外键。
投影(project):在诸多属性中选择一部分属性组成新的关系,如将关系学生信息投影为学号、姓名即得到上表中仅包含学号、姓名的列。
选择(select):按照一定条件选取特定元组,如选择上表中分数>80的元组。
笛卡尔积(交叉连接 cross join):将两个关系R和S中所有元组进行组合。
连接(join):从两个关系的笛卡尔积中选取满足给定条件的元组
自然连接(natural join):一种特殊的等值连接。它自动对两个关系中所有同名的属性进行等值比较,并在结果中去除重复的同名属性。
外连接(outer join):执行自然连接后,将舍弃的部分也加入,并且匹配失败处的属性用NULL代替。
除法运算(division):关系R除以关系S的结果为T,则T包含所有在R但不在S中的属性,且T的元组与S的元组的所有组合在R中。
函数依赖
描述属性之间存在的决定关系。最常见的是函数依赖。
- 函数依赖的定义
如果知道一个属性集 X 的值,就能唯一确定另一个属性集 Y 的值,则称 Y 函数依赖于 X,记作 X → Y。其中,X称为决定因子,Y称为依赖因子。
想象一张员工表:
| 员工ID | 姓名 | 部门 | 部门经理 |
|---|---|---|---|
| E001 | 张三 | 销售部 | 王经理 |
| E002 | 李四 | 销售部 | 王经理 |
| E003 | 王五 | 技术部 | 刘经理 |
| E004 | 赵六 | 技术部 | 刘经理 |
只要我知道了员工ID,我就能唯一确定他的姓名、部门和部门经理。
只要我知道了一个员工的部门,我就能唯一确定该部门的部门经理。
这种“知道X,就能唯一确定Y”的关系,就是函数依赖。
函数依赖的类型
- 完全函数依赖
如果Y函数依赖于X,但Y不函数依赖于X的任何一个真子集,则称Y完全函数依赖于X。例子:
(学号,课程号)-> 成绩成绩 完全依赖于 (学号,课程号) 这个组合。单独一个 学号 或 课程号 都无法决定 成绩。
记作:
(学号,课程号) —(F)→ 成绩- 部分函数依赖
如果Y函数依赖于X,但Y也函数依赖于X的一个真子集,则称Y部分函数依赖于X。例子:
(学号, 课程号)-> 姓名姓名其实只依赖于学号(即X的真子集),并不需要课程号,所以这是部分函数依赖。
这是一种数据冗余,是数据库设计时要避免的。
记作:
(学号, 课程号)-(P)-> 姓名- 传递函数依赖
当 X → Y,Y → Z,且 Y ↛ X 时,则称 Z 传递函数依赖于 X。例子:
学号 → 部门,部门 → 部门经理。根据这两个依赖,我们可以推导出
学号->部门经理。但这个决定是通过部门传递过来的。记作:
学号 —(T)→ 部门经理函数依赖的重要性
函数依赖是数据库规范化的理论基石。
目的:规范化旨在消除数据冗余、插入异常、删除异常和更新异常,从而设计出结构良好、高效的数据库。
过程:通过分析属性间的函数依赖关系,我们可以判断一个关系模式属于第几范式,并知道如何通过模式分解来提高到更高级的范式。
部分函数依赖 的存在意味着关系模式可能只满足第一范式,需要分解以达到第二范式。
传递函数依赖 的存在意味着关系模式可能只满足第二范式,需要分解以达到第三范式。
异常
在关系型数据库的语境下,异常 指的是由于糟糕的数据库设计(通常是没有进行充分的规范化)而导致的对数据进行增、删、改操作时出现的各种问题和不一致现象。 简单来说,异常就是“想做的事情做不到,或者做起来很麻烦,或者会带来副作用”。
异常主要分为三类:插入异常、删除异常 和 更新异常。
一个典型的“坏”设计例子
让我们用一个包含过多信息的单一表来演示所有这些异常。假设我们有一个 学生选课 表:
| 学号 | 姓名 | 学院 | 课程号 | 课程名 |
|---|---|---|---|---|
| S001 | 张三 | 计算机学院 | C01 | 数据库 |
| S001 | 张三 | 计算机学院 | C02 | 操作系统 |
| S002 | 李四 | 外国语学院 | C01 | 数据库 |
| S003 | 王五 | 计算机学院 | C03 | 计算机网络 |
这个表的主键是 (学号,课程号)。现在我们来看看它存在哪些问题。
- 插入异常
定义: 当你想插入一条数据时,由于缺少部分必要信息(如主键)而无法插入。
例子:
- 场景:学校新来了一个叫“赵六”的学生,他还没有选修任何课程。
- 问题:你能将他插入到这个表中吗?
- 答案:不能。因为主键是 (学号,课程号),而“赵六”还没有选课,课程号 为空。数据库不允许主键为空,所以这条记录无法插入。
- 后果:一个真实存在的学生实体,仅仅因为他没有行为(选课),就无法在系统中被记录。这是不合理的。
- 删除异常
定义:当你删除一条你想删除的信息时,意外地丢失了其他你不想丢失的信息。
例子:
- 场景:学生“李四”(S002)决定退选“数据库”这门课(C01)。我们需要删除 (S002, C01) 这条记录。
- 问题:删除这条选课记录后,会发生什么?
- 答案:李四的个人信息(S002,李四,外国语学院)也随之从数据库中彻底消失了。因为我们只有这一条记录存储了他的个人信息。
- 后果:我们只是想删除一个选课关系,却意外地丢失了一个学生的所有基本信息。如果想再次记录李四的选课,我们不得不重新输入他的个人信息,这很容易导致数据不一致。
- 更新异常
定义:当需要修改某个数据时,你不得不更新多行记录,如果遗漏了其中一些,就会导致数据不一致。
例子:
- 场景:“计算机学院”改名为“智能计算学院”。我们需要更新所有计算机学院学生的学院信息。
- 问题:如何更新?
- 答案:你必须找到表中所有
学院为“计算机学院”的记录(S001的两条记录和S003的一条记录),然后逐一更新。这是一个繁琐的过程。 - 后果:如果在更新过程中,只更新了S001的两条记录而漏掉了S003的记录,那么数据库中就出现了不一致:王五的学院信息还是旧的“计算机学院”,而张三的已经变成了新的“智能计算学院”。同一个学院在数据库中有两个不同的名字,这是非常严重的数据错误。
异常的根源
这些异常的根源在于数据冗余和依赖关系不正确。
冗余:在上述例子中,学生的 姓名 和 学院 信息相对于 学号 是重复的。只要学生选一门新课,这些信息就被重复存储一次。
错误依赖:
姓名 和 学院 实际上只依赖于主键的一部分 学号(即部分函数依赖),而不是完全依赖于整个主键(学号,课程号)。
这导致了更新和删除时的复杂性和风险。
针对上面的例子,我们可以将其分解为:
学生表:
学生(学号,姓名,学院)- 专门存储学生自身信息。课程表:
课程(课程号,课程名)- 专门存储课程信息。选课表:
选课(学号,课程号,成绩)- 专门存储学生和课程之间的“选课”关系。
这样分解后:
插入异常:可以新增一个尚未选课的学生(直接插入
学生表)。删除异常:删除李四的选课记录(从
选课表中删除),不会影响学生表中李四的基本信息。更新异常:更改学院名称只需在
学生表中更新一次该学院下所有学生的记录即可,不会出现不一致。
范式
范式理论是为了解决上面提出的由部分依赖造成的数据冗余和三种异常
高级别的范式依赖于低级别的范式,1NF是最低级别的范式。

这里以一个新的学生选课表为例介绍范式:
| 学号 | 姓名 | 系名 | 系主任 | 课程号 | 课程名 | 学分 | 成绩 |
|---|---|---|---|---|---|---|---|
| S001 | 张三 | 计算机系 | 王主任 | C01 | 数据库 | 4 | 90 |
| S001 | 张三 | 计算机系 | 王主任 | C02 | 操作系统 | 4 | 85 |
| S002 | 李四 | 数学系 | 李主任 | C01 | 数据库 | 4 | 95 |
| S003 | 王五 | 计算机系 | 王主任 | C03 |
这个表存在大量的数据冗余和之前提到的所有异常。
第一范式 1NF
定义:关系中每个属性都是不可再分的原子值。
检查:我们的表中,每一列的值都是单个值,没有出现复合值(如“电话1,电话2”)或多值(如在一个单元格里放“数据库,操作系统”)。所以,它已经满足 1NF。
要求:这是关系型数据库的基本要求,所有关系都必须至少满足第一范式。
第二范式 2NF
前提:必须首先满足第一范式。
定义:在满足1NF的基础上,每一个非主属性都必须完全函数依赖于整个主键(即不能存在部分函数依赖)。
分析我们的表:
主键:(学号,课程号)。这是一个复合主键。
非主属性:姓名,系名,系主任,课程名,学分,成绩。
现在我们分析依赖关系:
完全依赖:成绩 完全依赖于整个主键。要知道成绩,必须同时知道是哪个学生(学号)和哪门课程(课程号)。
部分依赖:
姓名和系名实际上只依赖于学号。只要知道学号(S001),就能知道姓名(张三)和系名(计算机系),而不需要课程号。课程名和学分只依赖于课程号。只要知道课程号(C01),就能知道课程名(数据库)和学分(4),而不需要学号。系主任依赖于系名,而系名又依赖于学号,所以系主任也是部分依赖于主键(通过传递,但其直接原因是部分依赖)。
问题:由于存在部分依赖,当同一个学生选修多门课程时,他的 姓名、系名、系主任 信息会被重复存储,造成数据冗余和更新异常。
解决方案(达到2NF):将部分依赖的属性拆分到新的表中。
将原表拆分成三个表:
学生表:存储学生自身信息。
学生(学号,姓名,系名,系主任)主键:
学号
课程表:存储课程自身信息。
课程(课程号,课程名,学分)主键:
课程号
选课表:存储学生和课程的关系及成绩。
选课(学号,课程号,成绩)主键:
(学号,课程号)外键:
学号引用学生表,课程号引用课程表。
现在,在 选课表 中,非主属性 成绩 完全依赖于整个主键 (学号,课程号)。学生表 和 课程表 的主键是单属性,不可能存在部分依赖。因此,所有表都满足 2NF。
第三范式
前提:必须首先满足第二范式。
定义:在满足2NF的基础上,任何非主属性都不传递函数依赖于主键(即所有非主属性必须直接依赖于主键)。
分析我们拆分后的表:
选课表 和 课程表 显然满足3NF,因为它们的非主属性都直接依赖于主键。
检查 学生表:
学生(学号,姓名,系名,系主任)主键是
学号。存在依赖关系:
学号 → 系名,系名 → 系主任。因此,
系主任传递函数依赖于学号(学号 → 系名 → 系主任)。
问题:这种传递依赖依然会导致数据冗余和更新异常。如果“计算机系”换了系主任,需要更新该系所有学生的记录。如果漏掉一个,就会导致数据不一致。
解决方案(达到3NF):消除传递依赖。将传递依赖的属性拆分到新的表中。
将 学生表 进一步拆分:
学生表:
学生(学号,姓名,系名)主键:
学号外键:
系名引用系列表
系列表(新表):
系列(系名,系主任)主键:
系名
现在,所有非主属性都直接依赖于其所在表的主键,不存在传递依赖。因此,所有表都满足 3NF。
经过规范化,我们得到了四个结构清晰、无冗余和异常的表:
学生(学号,姓名,系名)系列(系名,系主任)课程(课程号,课程名,学分)选课(学号,课程号,成绩)
总结
总结下来,三种范式分别解决了不同问题:
| 范式 | 要解决的问题 | 解决方法 |
|---|---|---|
| 1NF | 属性不是原子值 | 确保每列不可再分 |
| 2NF | 部分函数依赖导致的冗余 | 将部分依赖的属性拆分成新表 |
| 3NF | 传递函数依赖导致的冗余和更新异常 | 将传递依赖的属性拆分成新表 |
在大多数实际数据库设计中,达到第三范式 已经足够好了,它在数据冗余和查询性能之间取得了良好的平衡。
有时为了追求更高的查询性能(例如在数据仓库中),会故意降低范式标准,增加一些冗余,这称为反规范化。但这通常是在完全理解规范化带来的问题之后才采取的有意行为。
