数据库范式设计
数据库范式设计
范式是数据表设计的基本原则,又很容易被忽略。很多时候,当数据库运行了一段时间之后,我们才发现数据表设计得有问题。重新调整数据表的结构,就需要做数据迁移,还有可能影响程序的业务逻辑,以及网站正常的访问。所以在开始设置数据库的时候,我们就需要重视数据表的设计。
1. 数据库有哪些范式?
我们在设计关系型数据库模型的时候,需要对关系表各个字段之间联系的合理化程度进行定义,这就有了不同等级的规范要求,这些规范要求被称为范式(NF)。你可以把范式理解为,一张关系表的设计结构需要满足的某种设计标准的级别。
目前关系型数据库一共有 6 种范式,按照范式级别,从低到高分别是:1NF(第一范式)、2NF(第二范式)、3NF(第三范式)、BCNF(巴斯 - 科德范式)、4NF(第四范式)和 5NF(第五范式,又叫做完美范式)。
数据库的范式设计越高阶,冗余度就越低,同时高阶的范式一定符合低阶范式的要求,比如满足 2NF 的一定满足 1NF,满足 3NF 的一定满足 2NF,依次类推。
一般来说数据表的设计应尽量满足 3NF。但也不绝对,有时候为了提高某些查询性能,我们还需要破坏范式规则,也就是反范式设计。
2. 数据表中有哪些键?
范式的定义会使用到主键和候选键(因为主键和候选键可以唯一标识元组),数据库中的键(Key)由一个或者多个属性组成。
- 超键:能唯一标识元组的属性集叫做超键。
- 候选键:如果超键不包括多余的属性,那么这个超键就是候选键。
- 主键:用户可以从候选键中选择一个作为主键。
- 外键:如果数据表 R1 中的某属性集不是 R1 的主键,而是另一个数据表 R2 的主键,那么这个属性集就是数据表 R1 的外键。
- 主属性:包含在任一候选键中的属性称为主属性。
- 非主属性:与主属性相对,指的是不包含在任何一个候选键中的属性。
简单的举个栗子:NBA球员表和球队表。
球员表:球员编号、姓名、身份证号、年龄和球队编号;
球队表:球队编号、主教练和球队所在地。
对于球员表来说,超键就是包括球员编号或者身份证号的任意组合,比如(球员编号)(球员编号,姓名)(身份证号,年龄)等。候选键就是最小的超键,对于球员表来说,候选键就是(球员编号)或者(身份证号)。主键是我们自己选定,也就是从候选键中选择一个,比如(球员编号)。外键就是球员表中的球队编号。在球员表中,主属性是(球员编号)(身份证号),其他的属性(姓名)(年龄)(球队编号)都是非主属性。
3. 从 1NF 到 3NF
1NF 指的是数据库表中的任何属性都是原子性的,不可再分。不同的业务,对不可再分的要求也不一样。
**2NF 指的数据表里的非主属性都要和这个数据表的候选键有完全依赖关系。**所谓完全依赖不同于部分依赖,也就是不能仅依赖候选键的一部分属性,而必须依赖全部属性。
举个栗子:比如我们设计一张球员比赛表。
球员比赛表:球员编号、姓名、年龄、比赛编号、比赛时间和比赛场地,得分。
这里候选键和主键都为(球员编号,比赛编号),我们可以通过候选键来决定如下的关系:(球员编号, 比赛编号) → (姓名, 年龄, 比赛时间, 比赛场地,得分)
但是这个数据表不满足第二范式,因为数据表中的字段之间还存在着如下的对应关系:
(球员编号) → (姓名,年龄), (比赛编号) → (比赛时间, 比赛场地)
也就是说候选键中的某个字段决定了非主属性。你也可以理解为,对于非主属性来说,并非完全依赖候选键。
这样会产生怎样的问题呢?
- 数据冗余:如果一个球员可以参加 m 场比赛,那么球员的姓名和年龄就重复了 m-1 次。一个比赛也可能会有 n 个球员参加,比赛的时间和地点就重复了 n-1 次。
- 插入异常:如果我们想要添加一场新的比赛,但是这时还没有确定参加的球员都有谁,那么就没法插入。
- 删除异常:如果我要删除某个球员编号,如果没有单独保存比赛表的话,就会同时把比赛信息删除掉。
- 更新异常:如果我们调整了某个比赛的时间,那么数据表中所有这个比赛的时间都需要进行调整,否则就会出现一场比赛时间不同的情况。
为了避免出现上述的情况,我们可以把球员比赛表设计为下面的三张表。
球员表包含球员编号、姓名和年龄等属性;
比赛表包含比赛编号、比赛时间和比赛场地等属性;
球员比赛关系表包含球员编号、比赛编号和得分等属性。
这样的话,每张数据表都符合第二范式,也就避免了异常情况的发生。某种程度上 2NF 是对 1NF 原子性的升级。1NF 告诉我们字段属性需要是原子性的,而 2NF 告诉我们一张表就是一个独立的对象,也就是说一张表只表达一个意思。
**3NF 在满足 2NF 的同时,对任何非主属性都不传递依赖于候选键。**也就是说不能存在非主属性 A 依赖于非主属性 B,非主属性 B 依赖于候选键的情况。
我们用球员表举例子,这张表包含的属性包括球员编号、姓名、球队名称和球队主教练。现在,我们把属性之间的依赖关系画出来,如下图所示:
你能看到球员编号决定了球队名称,同时球队名称决定了球队主教练,非主属性球队主教练就会传递依赖于球员编号,因此不符合 3NF 的要求。
如果要达到 3NF 的要求,需要把数据表拆成下面这样:球员表的属性包括球员编号、姓名和球队名称;球队表的属性包括球队名称、球队主教练。
4. BCNF
如果数据表的关系模式符合 3NF 的要求,就不存在问题了吗?我们来看下这张仓库管理关系表:
在这个数据表中,一个仓库只有一个管理员,同时一个管理员也只管理一个仓库。我们先来梳理下这些属性之间的依赖关系。
仓库名决定了管理员,管理员也决定了仓库名,同时(仓库名,物品名)的属性集合可以决定数量这个属性。这样,我们就可以找到数据表的候选键是(管理员,物品名)和(仓库名,物品名)。
然后我们从候选键中选择一个作为主键,比如(仓库名,物品名)。
在这里,主属性是包含在任一候选键中的属性,也就是仓库名,管理员和物品名。非主属性是数量这个属性。
那如何判断这张表的范式等级呢?
首先,数据表每个属性都是原子性的,符合 1NF 的要求;其次,数据表中非主属性”数量“都与候选键全部依赖,(仓库名,物品名)决定数量,(管理员,物品名)决定数量,因此,数据表符合 2NF 的要求;最后,数据表中的非主属性,不传递依赖于候选键。因此符合 3NF 的要求。
既然这张表满足了 3NF 的要求,那么它就不存在问题了吗?我们来分析下面的情况:
- 增加一个仓库,但是还没有存放任何物品。根据数据表实体完整性的要求,主键不能有空值,因此会出现插入异常;
- 如果仓库更换了管理员,我们就可能会修改数据表中的多条记录;
- 如果仓库里的商品都卖空了,那么此时仓库名称和相应的管理员名称也会随之被删除。
所以,即便数据表符合 3NF 的要求,同样可能存在插入,更新和删除数据的异常情况。
那肿么办呢?
首先我们分析下造成这些异常的原因:主属性仓库名对于候选键(管理员,物品名)是部分依赖的关系,这样就有可能导致上面的异常情况。
因此,人们在 3NF 的基础上进行了改进,提出了 BCNF。它在 3NF 的基础上消除了主属性对候选键的部分依赖或者传递依赖关系。
为了满足 BCNF 的要求,我们需要将上面的仓库管理表拆分成下面两张表:
仓库表:仓库名, 管理员。
库存表:仓库名,物品名,数量。
这样就不存在主属性对于候选键的部分依赖或传递依赖。
5. 总结
- 1NF 需要保证表中每个属性都保持原子性;
- 2NF 需要保证表中的非主属性与候选键完全依赖;
- 3NF 需要保证表中的非主属性与候选键不存在传递依赖。
- BCNF 需要保证表中的主属性与候选键不存在部分依赖或者传递依赖。
练习
如果我们现在有一张学生选课表,包含的属性有学号、姓名、课程名称、分数、系别和系主任,如何修改,使它满足 3NF 的要求呢?
属性与候选键不存在传递依赖。
- BCNF 需要保证表中的主属性与候选键不存在部分依赖或者传递依赖。
练习
如果我们现在有一张学生选课表,包含的属性有学号、姓名、课程名称、分数、系别和系主任,如何修改,使它满足 3NF 的要求呢?