数据库中的范式
范式是一套严格的概念,本文从个人的理解解释范式,描述不一定严谨。
在关系型数据库中,关于数据表设计的基本原则和规范,被称为范式(Normal Form
)。在 SQL
中对范式不作强制要求,但范式却是减少数据冗余、便利数据维护非常有效的设计原则。范式分为五级,其中常用的为前三级,每级范式满足的条件需首先满足上一级。
需要注意的是,虽然范式可以有效减少数据冗余,但随范式等级的提高,可继续减少的冗余减少,随表的变多询问效率却在下降。因此常用的范式仅为前三级和 BC 范式,另外还提出了反范式牺牲一些规范来提高效率。
键
在介绍范式前,需要再次明确键的概念,在后面范式的描述中,需要使用。
- 超键:可唯一确定一条记录的字段组成的元组,可以只有一个字段。
- 候选键:没有子集是超键的超键。通俗地讲,候选键是没有冗余字段的用于唯一确定字段的元组。
- 主键:用户从候选键中选择一个(或不选)作为主键,包含多个字段的主键称为联合主键。用户应选择合适的候选键作主键。
- 外键:用于与外表联结的键,对应外表中的主键。
- 主属性:包含于任意候选键的字段。
- 非主属性:不包含于任何候选键的字段。
第一范式(1NF)
第一范式要求所有键具有原子性,即所有键都是不可分割的。
ID | 姓名 | 住址 |
---|---|---|
1 | 赵六 | 广东省广州市三元里78号 |
ID | 姓名 | 住址区域 | 详细住址 |
---|---|---|---|
1 | 赵六 | 广东省广州市三元里 | 三元里78号 |
在上面这个示例中,住址不满足原子性,因为我们可以将它划分为住址区域和详细住址两部分。不过虽然住址区域与详细住址依然是可分割的,但只要我们不将其分开用于不同用途,依然可以认为其满足原子性。
在实际应用中,应该保证所有字段在功能上具有原子性。
第二范式(2NF)
第二范式首先要求满足第一范式,第二范式要求消除对主键的部分依赖。
所谓部分依赖是指,当主键为联合主键时,字段仅由联合主键中的部分信息决定,与其余部分无关。由于所有键均满足第一范式,因此独立主键一定不被部分依赖。
课程编号 | 课程名称 | 讲师编号 | 讲师姓名 | 授课时间 |
---|---|---|---|---|
10001 | C 语言程序设计 | A0003 | 李四 | 周一 3-5 节 |
10001 | C 语言程序设计 | A0001 | 赵六 | 周二 6-7 节 |
10003 | 计算机网络 | A0001 | 赵六 | 周五 1-2 节 |
课程编号 | 讲师编号 | 授课时间 |
---|---|---|
10001 | A0003 | 周一 3-5 节 |
10001 | A0001 | 周二 6-7 节 |
10003 | A0001 | 周五 1-2 节 |
课程编号 | 课程名称 |
---|---|
10001 | C 语言程序设计 |
10003 | 计算机网络 |
讲师编号 | 讲师姓名 |
---|---|
A0001 | 赵六 |
A0003 | 李四 |
在修正前,我们可以将课程编号和讲师编号作为联合主键。但是显然的是,课程名称和讲师姓名仅仅分别依赖于课程编号和讲师编号,而与两者的联合无关,这就叫做关于主键的部分依赖。第二范式即在于消除部分依赖,从而去除部分冗余数据。
为了消除部分依赖,我们将部分依赖的关系单独建表,从而产生了三张表。如果需要获取原表的信息,就可以使用联结。
1 | SELECT |
第二范式的问题很容易避免,只需要避免使用联合主键即可避免部分依赖问题。在实际中,我们通常使用自增类型作为主键,或者将对应记录的唯一标识作为主键,例如课程编号。
第三范式(3NF)
第三范式首先要求满足第二满足。第三范式旨在消除传递依赖,即所有键应该直接依赖主键。
开课编号 | 课程编号 | 课程名称 | 讲师编号 | 讲师姓名 | 授课时间 |
---|---|---|---|---|---|
20230001 | 10001 | C 语言程序设计 | A0003 | 李四 | 周一 3-5 节 |
20230002 | 10001 | C 语言程序设计 | A0001 | 赵六 | 周二 6-7 节 |
20230003 | 10003 | 计算机网络 | A0001 | 赵六 | 周五 1-2 节 |
开课编号 | 课程编号 | 讲师编号 | 授课时间 |
---|---|---|---|
20230001 | 10001 | A0003 | 周一 3-5 节 |
20230002 | 10001 | A0001 | 周二 6-7 节 |
20230003 | 10003 | A0001 | 周五 1-2 节 |
课程编号 | 课程名称 |
---|---|
10001 | C 语言程序设计 |
10003 | 计算机网络 |
讲师编号 | 讲师姓名 |
---|---|
A0001 | 赵六 |
A0003 | 李四 |
这里我复用了在第二范式中的示例。只是简单地加了开课编号字段,就从不满足第二范式变成了不满足第三范式。此时原表的主键变成了开课编号,这里我使用树形图来描述这个依赖关系。
flowchart TD 1(开课编号) --> 2[课程编号] & 3[讲师编号] & 4[授课时间] 2 --> 课程名称 3 --> 讲师姓名
很明显,课程名称间接依赖于开课编号,讲师姓名也一样,这时我们同样需要将数据表拆解,从而消除传递依赖。
巴斯范式(BCNF)
我更习惯叫它 BC 范式,因为最初我并不知道 BC
的含义。BC 范式是人们在第三范式的基础上进行了改进提出的,也叫作巴斯-科德范式(Boyce-Codd Normal Form),因为被认为没有提出新的约束而不被认为是第四范式。
BC 范式首先要求满足第三范式。BC 范式要求任何存在依赖的字段必须依赖于一个超键,如果依赖的键不是一个超键,则不满足 BC 范式。BC 范式旨在消除键之间的传递依赖。在实际应用中,一般满足第三范式就会自动满足 BC 范式。BC 范式过于抽象,并且在实际中很难遇到符合第三范式但未达到 BC 范式的情况,我也没有找到一个好的示例。
一般范式仅介绍使用前三种,对于数据库设计的要求也只需满足第三范式或 BC 范式即可,后面也不再介绍第四和第五范式。
反范式
反范式即为违背规范,在前面第二范式的示例中也介绍了,为了满足范式的要求,我们需要将数据表分割从而去除一些冗余,但是当我们需要重新访问原来的数据时,通常需要使用联结,而使用联结需要消耗一些效率,如果将表分割的非常细,那么频繁使用联结会造成明显的效率下降。
反范式一般分为两个场景:
- 增加冗余,提升效率: 将频繁访问且稳定的字段在原表中存储一份,例如学生姓名一旦与学号绑定后一般不修改,但每次查询时通常都需要访问学生姓名,这时几乎每次查询都需要使用联结,将数据在原表中存储一份可以提高效率。另外不要存储不稳定的字段,因为假设学生姓名真的修改了,那么需要同时修改所有引用学生姓名的表,此时修改超过了查询,而且容易遗漏。
- 建立快照版本: 对于一些数据表,我们可能只关心数据的快照版本,如果内存充足,那么可以放心存储,因为快照数据永远不需要更新。例如:消费记录上只需要保存你的快照信息,即使你的信息发生修改,消费记录也无需更新,因为它并不关心。