数据库表设计范式
一、引言:为什么需要规范化?
在设计数据库表的过程中,相信大家或多或少会有如下疑问:我设计的表规范吗?这样设计字段对吗?有没有什么问题我在设计的时候发现不了,到了实际生产时才暴露出来?
为了解决上述问题,一个伟大的计划被提出来:有没有一种可能,只要我们在设计表的时候,遵从某些规范,就能尽量避免问题的出现?而今天要讲的数据库表设计范式,就是这样一种解决方案。
我们首先来看一个失败的设计方案:
学生选课表(学号, 学生姓名, 所在学院, 课程号, 课程名, 教师名, 成绩)
其中主键是(学号,课程号);乍一看,这张表设计的似乎挺好:一张表涵盖了挺多字段,起码挺热闹;然而,当我们插入一些数据的时候,灾难发生了:
| 学号 | 学生姓名 | 所在学院 | 课程号 | 课程名 | 教师名 | 成绩 |
|---|---|---|---|---|---|---|
| 001 | 张三 | 软件学院 | C01 | 数据库 | 王老师 | 90 |
| 001 | 张三 | 软件学院 | C02 | 操作系统 | 李老师 | 85 |
| 001 | 张三 | 软件学院 | C03 | 计算机网络 | 赵老师 | 88 |
可以看到,001 -> 张三, 软件学院这部分数据重复出现了很多次,这在数据库表设计中称为数据冗余;同理,如果我们想要修改某一列数据,比如说修改张三所在学院,就要同时修改三行记录中的同一处数据;这叫做更新异常;
如果上述问题还在忍受范围内,只要脸皮厚,多存一点数据、多改几次表并不是完全不可以,下面的问题就是灾难中的灾难:如果我们想插入一条课程:综合程序设计,我们会惊奇的发现:插入不了!这是因为没有学生选课时,我们无法单独插入课程信息,这叫做插入异常;同理,如果删除最后一条选课记录时,我们同样会惊奇的发现:这门课的课程信息也丢失,这叫做删除异常。
其实要解决上述问题很简单:拆表。将原有表拆成
学生表(学号, 学生姓名, 所在学院)
课程表(课程号, 课程名, 教师名)
选课表(学号, 课程号, 成绩)
上述问题就会迎刃而解。可接下来的问题是:我怎么知道该如何拆表呢?规范来说,我们的核心目的是:
通过合理拆分关系模式,减少数据冗余,避免插入、删除、更新异常,提高数据库结构的合理性。
而范式,就是通过一系列规则来告诉我们该如何科学拆表。
二、第一范式
在正式介绍范式前,我们必须要了解一些基本的知识:
| 中文术语 | 含义 | 也常被叫作 |
|---|---|---|
| 超码 | 能唯一确定一条记录的属性集 | 超键 |
| 候选码 | 最小的超码 | 候选键 |
| 主码 | 从候选码中选出来作为主键的码 | 主键 |
| 主属性 | 出现在某个候选码中的属性 | 主属性,一般不叫“主键属性” |
| 非主属性 | 不出现在任何候选码中的属性 | 非主属性 |
有人可能有疑惑:主键我明白,候选键我知道,超键是个啥玩意?其实这三者很类似,只不过超键的范围要大一点,可以这么看:
超键 ⊃ 候选键 ⊃ 主键
简单对比一下:
-
超键:能唯一定位一行就行,可以有多余字段。
-
候选键:没有多余字段的超键。
-
主键:从候选键中选出来,真正作为表中主要标识的那个键。
接下来我们看看什么是第一范式。
第一范式(1NF)要求:关系中的每个属性值都必须是原子的,不可再分。
比如这个例子:学生表(学号, 姓名, 联系方式),该关系满足1NF吗?显然不满足,因为联系方式可以拆分为电话,邮箱等等:
| 学号 | 姓名 | 联系方式 |
|---|---|---|
| 1001 | 张三 | 13800000000, 123456@qq.com |
| 1002 | 李四 | 13900000000, 微信: lisi123 |
一个字段里塞了多个联系方式,就不满足1NF。
三、第二范式
在满足了第一范式的基础上,我们来看看第二范式。
第二范式(2NF)要求:在满足 1NF 的基础上,所有非主属性都必须完全函数依赖于候选键,不能只依赖候选键的一部分。
这里的非主属性前文提到过,就是不出现在任何候选键中的属性。
例如这个关系选课表(学号, 课程号, 学生姓名, 学院, 课程名, 成绩),主键和候选键都是(学号, 课程号)能唯一确定一行,如果其他列都能满足必须完全依赖(学号, 课程号),那么这个关系满足2NF。然而我们发现,学生姓名、学院 只依赖 学号,课程名 只依赖 课程号,都属于部分依赖,因此这个关系不满足2NF。如果改造,可以这么改:
学生表(学号, 学生姓名, 学院)
课程表(课程号, 课程名)
选课表(学号, 课程号, 成绩)
需要注意的是,2NF针对的是联合主键,如果一个关系的候选键只有一个属性,那么它天然不存在“部分依赖”问题,因此只要满足 1NF,通常就满足 2NF。简单说,就是只要你的关系里的主键只有一个属性,自动满足2NF。
四、第三范式
第三范式(3NF)要求:在满足 2NF 的基础上,非主属性不能传递依赖于候选键。
这里的重点是传递依赖,什么意思呢?
我们还是以学生表举例:学生表(学号, 学生姓名, 所在院系, 院长),我们以学号为主键,此时该关系存在以下依赖:
学号 -> 学生姓名, 所在院系
所在院系 -> 院长
这里可以看到:尽管学号直接决定了学生姓名, 所在院系,但院长却是由所在院系间接决定的,因此它违反了3NF。要改的话可以拆成:
学生表(学号, 学生姓名, 所在院系)
学院表(所在院系, 院长)
在实际业务开发中,很多表设计到 3NF 基本已经足够,但为了应对期末考试,接下来还要介绍另外两种范式。
五、BCNF范式
巴斯-科德范式(BCNF)要求:对于关系模式 R 中的每一个非平凡函数依赖 X → Y,X 都必须是 R 的超键。
这里出现了一个新名词:非平凡函数依赖,它指的是X → Y并且 Y 不是 X 的子集。例如:学号 → 姓名是非平凡函数依赖,但:{学号, 姓名} → 姓名是平凡函数依赖,因为右边的 姓名 本来就包含在左边里面。
我们举一个经典的满足 3NF 但不满足 BCNF 的例子:学生选课教师表(学号, 课程号, 教师),其中一个学生可以选多门课,一门课可以由多个老师教,每个老师只教一门课;显而易见主键是(学号, 课程号),函数依赖为(学号, 课程号) → 教师,教师 → 课程号,由第二个函数依赖可看出:教师决定课程号,但教师并不是超键,因为教师字段无法确定唯一行;因此这个关系不满足 BCNF,可以拆为:
教师课程表(T, C)
学生教师表(S, T)
需要注意的是,BCNF 作为增强 3NF,相比于 3NF 更严格,但分解后可能导致:
- 某些函数依赖无法直接保持
- 查询时需要更多连接操作
- 表结构更加分散
因此实际工程中不一定盲目追求 BCNF。
六、第四范式
第四范式(4NF)要求:如果关系满足BCNF范式,并消除了多值函数依赖,该关系满足第4范式。
第四范式主要用来处理多值依赖,这里的多值依赖是数据库规范化里的一种依赖关系,主要用来描述:一个属性确定的不是一个值,而是一组彼此独立的多个值。
例如:
学生(学号, 爱好, 语言)
如果一个学生有多个爱好,也会多门语言,那么爱好和语言之间没有直接关系,但会产生组合冗余。
可以拆成:
学生爱好表(学号, 爱好)
学生语言表(学号, 语言)
七、实际设计时怎么用范式?
- 先保证字段原子性:满足 1NF
- 如果有联合主键,检查是否存在部分依赖:满足 2NF
- 检查非主属性是否依赖另一个非主属性:满足 3NF
- 复杂关系再考虑 BCNF / 4NF
- 工程中允许有条件反规范化,但要知道自己为什么这么做
