• 作者:老汪软件技巧
  • 发表时间:2024-09-05 00:02
  • 浏览量:

前言

凡做事,必有方法。这个方法也可以称为框架,遇事我们将框架套上去,这事儿就成了。DB设计也是如此,业务中不能够随心所欲,走先污染后治理的路子。当我们在做数据表结构设计的时候,需要遵循一定的规范,这些规范就是我们常说的范式,它帮助我们在设计出更为合理且可扩展的表结构。

在数据库设计范式中,众所周知的是三大范式(1NF、2NF、3NF),这也是实际业务中至少需要遵循的标准,除此之外还有BC范式、第四范式4NF、第五范式5NF。

三大范式

三大范式是业务开发中遵循的最低标准,每个范式之间是递进关系,也就是后面一个范式要基于前一个范式成立的基础上才有意义,不能倒反天罡,例如:

把大象放进冰箱需要三步:首先打开门,然后将大象放进冰箱,最后关上门。

每一步是要以前面一步完成作为基础,你不能先放进冰箱然后再打开门,这是没有意义且行不通的。明白这个概念后我们再来通俗理解这几个范式。

第一范式

要求保证数据表中每一列具有原子性,也就是存储数据列是具有不可再分性,什么意思呢?举例说明。

| 员工ID | 员工 | 技能 | 部门               |
|--------|------|------|--------------------|
| 1      | 张三,男,30 | Vue   | 技术部     |
| 1      | 张三,男,30 | React   | 技术部     |
| 1      | 张三,男,30 | Node   | 技术部     |
| 2      | 李四,女,22 | Word   | 财务部   |
| 2      | 李四,女,22 | Excel  | 财务部    | 
| 3      | 王五,男,35 | Docker   | 架构部     |
| 3      | 王五,男,35 | K8s   | 架构部     |

上面案例中,员工字段列包含了姓名、性别、年龄三种类型的信息,显然不符合第一范式:原子性。为了符合第一个范式,将员工字段拆分三个字段进行储存,应该将表结构改为:

| 员工ID | 姓名 | 性别 | 年龄 | 技能 | 部门   |
|--------|------|------|------|------|--------|
| 1      | 张三 || 30   | Vue | 技术部 |
| 1      | 张三 || 30   | React | 技术部 |
| 1      | 张三 || 30   | Node | 技术部 |
| 2      | 李四 || 22   | Word | 财务部 |
| 2      | 李四 || 22   | Excel | 财务部 |
| 3      | 王五 || 35   | Docker | 架构部 |
| 3      | 王五 || 35   | K8s | 架构部 |

将一个字段拆分三个字段之后,此时这个员工表就符合了数据库第一范式。

好,先就此打住!如果开发人员因为前期设计缺少考虑或未按照DB范式设计,硬是生成了这种表结构,那会产生什么问题呢?

满足第一范式后接着继续往下看。

第二范式

一句话理解:每一列都与主键列存在依赖关系。

解释:在第一范式基础上,第二范式要求数据表中每一列的数据,都必须依赖于主键。意味着每一列的数据都与主键存在关系,保证了一张表只能存储同一类型的数据,一张表只做一件事情,依旧来看上面案例。

| 员工ID | 姓名 | 性别 | 年龄 | 技能 | 部门   |
|--------|------|------|------|------|--------|
| 1      | 张三 || 30   | Vue | 技术部 |
| 1      | 张三 || 30   | React | 技术部 |
| 1      | 张三 || 30   | Node | 技术部 |
| 2      | 李四 || 22   | Word | 财务部 |
| 2      | 李四 || 22   | Excel | 财务部 |
| 3      | 王五 || 35   | Docker | 架构部 |
| 3      | 王五 || 35   | K8s | 架构部 |

技能字段与员工信息似乎属于不同属性的数据,上面表的设计显然不能把员工ID作为主键,应该确保一张表中只储存一种类型的数据。应该拆分下面几张表:

员工信息表:

| 员工ID | 姓名 | 性别 | 年龄 | 部门 |
|--------|------|------|------|--------|
| 1      | 张三 || 30   | 技术部      |
| 2      | 李四 || 22   | 财务部      |
| 3      | 王五 || 35   | 架构部      |

技能表:

| 技能ID | 技能   |
|--------|--------|
| 1      | Vue    |
| 2      | React  |
| 3      | Node   |
| 4      | Word   |
| 5      | Excel  |
| 6      | Docker |
| 7      | K8s    |

员工与技能关联表:

| 员工ID | 技能ID |
|--------|--------|
| 1      | 1      |
| 1      | 2      |
| 1      | 3      |
| 2      | 4      |
| 2      | 5      |
| 3      | 6      |
| 3      | 7      |

这样一拆分就清晰多了,员工表和技能表分别存储各自信息,通过关联表维护两者关系,满足了每张表只负责做一件事情,表字段也只与当前主键列存在关系。

此时,员工表和技能表都以自增ID作为主键,确保了每条记录的“唯一性”,在查询时将主键作为唯一索引能够加快查询效率。

OK!那如果按照修改前的表结构存储,会有什么问题?

满足了第二范式后,似乎我们的表设计已经完美了。按照目前的业务复杂度的确如此,接下来新增一些字段,来看第三范式解决了什么问题。

第三范式

一句话理解:非主键列之间不能存在依赖关系。

解释:要求数据表中的非主键列的字段,不能与其他非主键列存在依赖关系,说白了内部不能搞小组织。

在上面满足第二范式的员工表中新增一列部门老大字段:

| 员工ID | 姓名 | 性别 | 年龄 | 部门   | 部门老大 |
|--------|------|------|------|--------|----------|
| 1      | 张三 || 30   | 技术部 | 张总   |
| 2      | 李四 || 22   | 财务部 | 李总   |
| 3      | 王五 || 35   | 架构部 | 王总   |

新增字段之后,此时表结构依旧满足第一范式和第二范式,但看部门与部门老大两个非主键字段就产生了依赖关系呀,一个员工归属于谁管,应该是由这个员工在哪个部门决定,有了部门才有部门老大这个岗位,员工只需存储该部门ID即可,因此需要进一步调整表结构。

理清楚后把表结构拆分如下:

员工表:

| 员工ID | 姓名 | 性别 | 年龄 |
|--------|------|------|------|
| 1      | 张三 || 30   |
| 2      | 李四 || 22   |
| 3      | 王五 || 35   |

部门表:

| 部门ID | 部门名称 | 部门老大 |
|--------|----------|----------|
| 1      | 技术部   | 张总   |
| 2      | 财务部   | 李总   |
| 3      | 架构部   | 王总   |

员工部门关联表:

| 员工ID | 部门ID |
|--------|--------|
| 1      | 1      |
| 2      | 2      |
| 3      | 3      |

非得拆么?如果不拆分表字段,又会有什么问题?

实际业务中删除数据一般采用软删而不是物理删除,符合第三范式情况下更容易进行标记删除。

BC范式

BC范式名为xxx,是对第三范式的补充,为什么需要补充呢?因为第三范式没有覆盖某些场景,比如联合主键的场景。来看另一个例子:

想象一下,有一个学校的成绩记录表,这个表记录了每个学生上不同课程的成绩,以及这些课程是由哪位老师教的。表中每一行都代表一个学生在某个课程上的一个成绩记录。

| 学生ID | 课程ID | 教师ID | 成绩 |
|--------|--------|--------|------|
| 1      | 101    | 201    | 85   |
| 1      | 102    | 202    | 90   |
| 2      | 101    | 201    | 75   |
| 2      | 102    | 202    | 80   |

在这个表中:

再来看看能不能满足BC范式,它的核心思想是:

每个数据项都应该直接依赖于整个主键,而不是主键的一部分。

我们把学生ID + 课程ID作为主键,那么教师ID和成绩应该直接依赖于这个主键。但是,实际上教师ID并不直接依赖于学生ID + 课程ID,而是依赖于课程ID。

为了满足BC范式,我们可以把原来的表拆分成两个表:

课程教师表:

| 课程ID | 教师ID |
|--------|--------|
| 101    | 201    |
| 102    | 202    |

学生成绩表:

| 学生ID | 课程ID | 成绩 |
|--------|--------|------|
| 1      | 101    | 85   |
| 1      | 102    | 90   |
| 2      | 101    | 75   |
| 2      | 102    | 80   |

第四范式

第四范式是为了解决多值依赖问题而提出的,在此之前先了解什么是多值依赖?

多值依赖的问题至少在三个字段以上的表结构中才会出现,可以想象成一对多、多对一关系,一个字段的值需要通过多个字段才能够确定。

来看一个经典的例子:用户、角色、权限问题。

| user_id | user_name | user_sex | role  | permission |
|---------|-----------|----------|-------|------------|
| 1       | Alice     || ADMIN | MANAGE     |
| 2       | Bob       || USER  | VIEW       |
| 3       | Charlie   || ADMIN | EDIT       |
| 4       | David     || USER  | VIEW       |
| 5       | Eve       || USER  | VIEW       |
| 6       | Frank     || ADMIN | MANAGE     |

上述表中,先来看是否满足前面的范式:

从严格来说user_id、role、permission作为联合主键,也可以说它们之间存在依赖关系,比如permission字段依赖role字段,但这种划分的界限不是绝对的,具体以解决实际业务问题作为标准。我们这里暂且认为是满足BC范式。

用户permission字段没法只根据role字段来决定,也无法只根据user_id来决定,而是要role与user_id两个字段共同决定,这就是所谓的多值依赖问题。

再来看另外一个例子,下表是存储学生ID、课程ID、讲师名字、成绩相关信息。

| StudentID | CourseID | Professor | Grade |
|-----------|----------|----------|-------|
| 1         | 101      | Smith    | A     |
| 1         | 102      | Jones    | B     |
| 2         | 101      | Smith    | A     |
| 2         | 103      | Brown    | B     |

以StudentID + CourseID作为联合主键,是满足上面的几大范式的。

但是会有个问题,StudentID + CourseID无法确定是哪个具体的讲师,只能确定成绩字段,而课程ID与讲师存在依赖关系,一个讲师可能教多个课程,形成一对多关系,这就是多值依赖问题。

消除这种问题的解决方案就是通过拆分表结构,比如前面的用户权限问题,拆分为大名鼎鼎的权限五表。

用户表(`Users`)
| user_id | user_name | user_sex |
|---------|-----------|----------|
| 1       | Alice     ||
| 2       | Bob       ||
| 3       | Charlie   ||
| 4       | David     ||
| 5       | Eve       ||
| 6       | Frank     ||
角色表(`Roles`)
| role_id | role_name |
|---------|-----------|
| 1       | ADMIN     |
| 2       | USER      |
权限表(`Permissions`)
| permission_id | permission_name |
|---------------|-----------------|
| 1             | MANAGE          |
| 2             | VIEW            |
| 3             | EDIT            |
用户角色表(`UserRoles`)
| user_id | role_id |
|---------|---------|
| 1       | 1       |
| 2       | 2       |
| 3       | 1       |
| 4       | 2       |
| 5       | 2       |
| 6       | 1       |
角色权限表(`RolePermissions`)
| role_id | permission_id |
|---------|---------------|
| 1       | 1             |
| 1       | 3             |
| 2       | 2             |

而学生课程成绩表也调整为如下:


学生课程成绩表:
| StudentID | CourseID | Grade |
|-----------|----------|-------|
| 1         | 101      | A     |
| 1         | 102      | B     |
| 2         | 101      | A     |
| 2         | 103      | B     |
课程讲师表:
| CourseID | Professor |
|----------|-----------|
| 101      | Smith     |
| 102      | Jones     |
| 103      | Brown     |

第五范式

来看最后一个范式:第五范式,也被称为完美范式。它是为了消除连接依赖问题的,而连接依赖 是指数据可以通过多个表的连接来恢复的一种依赖关系。

这个概念似乎有些抽象,我查了一些资料之后,似乎依旧没能找到一个合适的例子来解释说明。但有一点可以确定的是,解决这类问题依旧是对表结构进行拆分,拆成粒度更细的表,解决数据冗余等问题。

更多的就不强行演示了,有知道的朋友可以留言。

反范式设计

经过上面的一些列案例演示,不难看到我们在满足每一个范式时,会伴随着数据表变得更加粒度化,也就是表越来越多了,这也会产生一些问题:

所以业务中我们会面临范式选择问题,根据自身的业务场景为其设计合适级别的范式,而并不一定要求100%遵循所有范式,这种设计思想就是反范式设计。如果反范式设计能够提升业务查询效率,减少连表查询的次数,那这个设计就是最优的。

总结

本篇总阐述了几大范式的概念及其设计思想,一定程度上遵循设计有助于我们设计出可维护、可扩展的表结构,但具体场景具体分析,通常情况下我们能够满足第一二三范式,权衡复杂性和扩展性的基础上,这个级别的范式是比较通用的。

对于打破范式设计的思想,如果对业务有利,也可以进行反范式设计。

以上的这些都是DB设计套路(框架),可以给我们业务开发中指定一个方向,需要使用时直接套上去即可。