范式是一套严格的概念,本文从个人的理解解释范式,描述不一定严谨。

在关系型数据库中,关于数据表设计的基本原则和规范,被称为范式(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
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT 
M.courseId,
C.name AS courseName,
M.lecturerId,
L.name AS lecturerName,
M.classTime
FROM
classInfo AS M
LEFT JOIN
courseInfo AS C
ON M.courseId = C.id
LEFT JOIN
lecturerInfo AS L
ON M.lecturerId = L.id

第二范式的问题很容易避免,只需要避免使用联合主键即可避免部分依赖问题。在实际中,我们通常使用自增类型作为主键,或者将对应记录的唯一标识作为主键,例如课程编号。


第三范式(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 李四

这里我复用了在第二范式中的示例。只是简单地加了开课编号字段,就从不满足第二范式变成了不满足第三范式。此时原表的主键变成了开课编号,这里我使用树形图来描述这个依赖关系。

很明显,课程名称间接依赖于开课编号,讲师姓名也一样,这时我们同样需要将数据表拆解,从而消除传递依赖。


巴斯范式(BCNF)

我更习惯叫它 BC 范式,因为最初我并不知道 BC 的含义。BC 范式是人们在第三范式的基础上进行了改进提出的,也叫作巴斯-科德范式(Boyce-Codd Normal Form),因为被认为没有提出新的约束而不被认为是第四范式。

BC 范式首先要求满足第三范式。BC 范式要求任何存在依赖的字段必须依赖于一个超键,如果依赖的键不是一个超键,则不满足 BC 范式。BC 范式旨在消除键之间的传递依赖。在实际应用中,一般满足第三范式就会自动满足 BC 范式。BC 范式过于抽象,并且在实际中很难遇到符合第三范式但未达到 BC 范式的情况,我也没有找到一个好的示例。

一般范式仅介绍使用前三种,对于数据库设计的要求也只需满足第三范式或 BC 范式即可,后面也不再介绍第四和第五范式。


反范式

反范式即为违背规范,在前面第二范式的示例中也介绍了,为了满足范式的要求,我们需要将数据表分割从而去除一些冗余,但是当我们需要重新访问原来的数据时,通常需要使用联结,而使用联结需要消耗一些效率,如果将表分割的非常细,那么频繁使用联结会造成明显的效率下降。

反范式一般分为两个场景:

  • 增加冗余,提升效率: 将频繁访问且稳定的字段在原表中存储一份,例如学生姓名一旦与学号绑定后一般不修改,但每次查询时通常都需要访问学生姓名,这时几乎每次查询都需要使用联结,将数据在原表中存储一份可以提高效率。另外不要存储不稳定的字段,因为假设学生姓名真的修改了,那么需要同时修改所有引用学生姓名的表,此时修改超过了查询,而且容易遗漏。
  • 建立快照版本: 对于一些数据表,我们可能只关心数据的快照版本,如果内存充足,那么可以放心存储,因为快照数据永远不需要更新。例如:消费记录上只需要保存你的快照信息,即使你的信息发生修改,消费记录也无需更新,因为它并不关心。