数据库三范式示例理解
理解数据库范式和函数依赖是数据库设计的基石。这些概念很抽象,但我们可以通过一个从混乱到有序的实际例子,一步步把它们弄清楚。
我们就用一个学校的“学生选课信息”表作为例子,看看它是如何从一个糟糕的设计,一步步通过范式变得“健康”的。
基础:函数依赖 (Functional Dependency)
在理解范式之前,必须先理解函数依赖,因为它是判断是否符合范式的“尺子”。
通俗解释:在一个关系(表)中,如果通过属性A的值,我们总能唯一地确定属性B的值,那么我们就说“B函数依赖于A”,记作 A -> B。
A叫做决定因素 (Determinant)B叫做依赖部分 (Dependent)
生活化比喻:就像自动售货机。你按下一个按钮(决定因素),就会出来一个特定的商品(依赖部分)。
按钮编号 -> 商品名称学号 -> 姓名(知道学号,就能唯一确定姓名)身份证号 -> {姓名, 年龄, 性别}(知道身份证号,就能唯一确定这些信息)
完全函数依赖 vs. 部分函数依赖
这个概念是理解第二范式的关键,它只在主码是复合码(由多个属性组成)时有意义。
完全函数依赖:一个属性必须依赖于主码的全部属性,而不是部分属性。
- 例子:主码是
{学号, 课程号}。属性成绩依赖于{学号, 课程号}。因为只知道学号无法确定成绩(不知道是哪门课的),只知道课程号也无法确定成绩(不知道是哪个学生的),必须两者都知道才行。所以,成绩完全函数依赖于{学号, 课程号}。
- 例子:主码是
部分函数依赖:一个属性只依赖于主码的一部分属性。
- 例子:主码还是
{学号, 课程号}。属性姓名只依赖于学号,和课程号没关系。这就是部分函数依赖。
- 例子:主码还是
第一范式 (1NF)
目标:确保属性的原子性,即表中的每个单元格都不可再分。
“人话”规则:表格里不能有“格中格”。每一列的值都必须是单一值,不能是集合或列表。
例子:一个糟糕的、不符合1NF的表
| 学号 | 姓名 | 选课信息 |
|---|---|---|
| S01 | 张三 | { (C01, C语言, 李老师), (C02, 数据库, 王老师) } |
| S02 | 李四 | { (C02, 数据库, 王老师) } |
问题:“选课信息”这一列包含了多个课程,是一个集合,不是原子值。这违反了1NF。
如何修正:拆分!把集合拆成多行。
修正后,符合1NF的表
| 学号 | 姓名 | 课程号 | 课程名 | 教师名 |
|---|---|---|---|---|
| S01 | 张三 | C01 | C语言 | 李老师 |
| S01 | 张三 | C02 | 数据库 | 王老师 |
| S02 | 李四 | C02 | 数据库 | 王老师 |
现在,每个单元格都是单一值了。我们达到了1NF。但是,这个表仍然很糟糕,充满了数据冗余(张三的名字、数据库课程名、王老师的名字都重复了)。这会导致更新异常(比如王老师改名了,你需要修改多行,很容易漏掉)。
第二范式 (2NF)
前提:必须首先满足1NF。
目标:消除部分函数依赖。
“人话”规则:一张表只说一件事。表中的所有非主属性,都必须完全依赖于整个主码,而不是主码的一部分。
例子:分析我们符合1NF的表
- 确定主码:要唯一确定一行,只知道
学号不行(一个学生有多门课),只知道课程号也不行(一门课有多个学生)。所以,主码是{学号, 课程号}。 - 分析函数依赖:
姓名-> 只依赖于学号。(学号->姓名)课程名-> 只依赖于课程号。(课程号->课程名)教师名-> 只依赖于课程号。(假设一门课只有一个老师)
- 发现问题:
姓名,课程名,教师名这些非主属性,都只依赖于主码的一部分,存在严重的部分函数依赖。这违反了2NF。
如何修正:拆分!把描述不同事物的属性拆分到不同的表中。
- 跟“学生”相关的信息,单独放一张表。
- 跟“课程”相关的信息,也单独放一张表。
- 剩下的“学生和课程的关系”,保留在原表中。
修正后,符合2NF的表
学生表 (Student)
| 学号 (主码) | 姓名 |
|---|---|
| S01 | 张三 |
| S02 | 李四 |
课程表 (Course)
| 课程号 (主码) | 课程名 | 教师名 |
|---|---|---|
| C01 | C语言 | 李老师 |
| C02 | 数据库 | 王老师 |
选课表 (SC)
| 学号 (主码/外码) | 课程号 (主码/外码) |
|---|---|
| S01 | C01 |
| S01 | C02 |
| S02 | C02 |
现在,数据冗余大大减少。每个表都只描述一件事:学生表只说学生,课程表只说课程。我们达到了2NF。但是,课程表里似乎还有问题…
第三范式 (3NF)
前提:必须首先满足2NF。
目标:消除传递函数依赖。
传递函数依赖:如果 A -> B 且 B -> C,那么存在 A -> C 的传递依赖。这里A是主码,B和C都是非主属性。
“人话”规则:表里的所有非主属性,都必须直接依赖于主码,不能“隔着”另一个非主属性去依赖主码。
例子:分析我们符合2NF的课程表
课程表 (Course)
| 课程号 (主码) | 课程名 | 教师名 |
|---|---|---|
| C01 | C语言 | 李老师 |
| C02 | 数据库 | 王老师 |
- 假设我们再加一个“教师办公室”列:
| 课程号 (主码) | 课程名 | 教师名 | 教师办公室 |
|---|---|---|---|
| C01 | C语言 | 李老师 | 办公室A |
| C02 | 数据库 | 王老师 | 办公室B |
- 分析函数依赖:
课程号 -> 课程名(直接依赖)课程号 -> 教师名(直接依赖,假设一门课只有一个老师)- 但是,
教师名 -> 教师办公室!
- 发现问题:
教师办公室并不直接依赖于课程号。它依赖于教师名,而教师名才依赖于课程号。这就是传递函数依赖 (课程号 -> 教师名 -> 教师办公室)。这违反了3NF。 - 这会带来什么问题? 如果王老师同时教了10门课,他的办公室信息就会重复10次。如果他换了办公室,你需要修改10行。
如何修正:再拆分!把“教师”这个独立的事物也拆出去。
修正后,符合3NF的表
课程表 (Course) (现在只说课程)
| 课程号 (主码) | 课程名 | 教师号 (外码) |
|---|---|---|
| C01 | C语言 | T01 |
| C02 | 数据库 | T02 |
教师表 (Teacher)
| 教师号 (主码) | 教师名 | 教师办公室 |
|---|---|---|
| T01 | 李老师 | 办公室A |
| T02 | 王老师 | 办公室B |
学生表 (Student) (不变)
选课表 (SC) (不变)
现在,所有的非主属性都直接且完全地依赖于它们所在表的主码,没有部分依赖,也没有传递依赖。我们的数据库设计达到了3NF,变得非常健康、不易出错。
总结
| 范式 | “人话”规则 | 技术规则 | 解决方法 |
|---|---|---|---|
| 1NF | 表格里不能有“格中格” | 属性必须是原子的,不可再分 | 拆分行(消除重复组) |
| 2NF | 一张表只说一件事 | 消除非主属性对主码的部分函数依赖 | 拆分表(消除部分依赖) |
| 3NF | 属性必须直接依赖主码,不能“隔山打牛” | 消除非主属性对主码的传递函数依赖 | 再拆分表(消除传递依赖) |

