数据库基本范式的实现条件及方法
本文介绍了数据库几个基本范式的实现条件;并且,还通过一个具体的例子讲解了对一个非规范化表进行改造、使之满足各个基本范式的过程。
引言
数据库规范化(normalization),又称正规化、标准化,是数据库设计中对表结构进行调整、使之满足关系表的格式要求并减少数据冗余的过程。规范化的主要意义在于通过避免更新异常(update anomaly) 1 以提高数据一致性,同时也有利于数据模型的扩展。
1 可以进一步分为插入异常(insertion anomaly)、删除异常(deletion anomaly)和修改异常(modification anomaly)。
2 1NF是形成关系表(relation)的充要条件,即满足1NF的表都是关系表,且关系表均满足1NF。另外,只要满足1NF,表就可以被视为已经得到规范化(normalised)。
表的规范程度从低到高可以分为:非规范化形式(UNF);第一范式(1NF) 2 ;第二范式(2NF);第三范式(3NF);主键范式(EKNF);Boyce-Codd范式(BCNF);第四范式(4NF);关键元组范式(ETNF);第五范式(5NF);域键范式(DKNF);第六范式(6NF) (Date 2019) 。满足较高范式的表也必定满足较低的范式,例如:满足3NF的表也必定满足2NF和1NF。
虽然数据表的范式越高,数据一致性就越容易得到保证,但其代价是表数量以及查询复杂度的增加。因此,实际应用中并不应该盲目地追求高范式;多数情况下,只要达到3NF即可 (Connolly 和 Beg 2015; Carlos Coronel 和 Steven Morris 2018; Date 2019) ;更高等级的范式通常只具备理论研究价值。
本文的主要目的在于介绍主要范式的实现条件,以及对低范式表进行规范化、从而使之达到更高范式的方法。为了使内容更具参考价值和可操作性,本文将引入一个具体的例子进行讲解;考虑到实用性,本文对相关内容的介绍将止于3NF、而不会涉及更高等级的范式。另外,本文将假设读者已经对键(key)和函数依赖(functional dependency)等数据库规范化的理论基础概念有所了解 3 。
3 具体包括:候选键(candidate key)、主键(primary key)、复合键(composite key)、部分依赖(partial dependency)和传递依赖(transitive dependency)等
一个UNF表实例
图 1 所示的PROJECT_UNF
表记录了某家公司的项目数据(Carlos Coronel 和 Steven Morris 2018),其中:每行代表一个项目;每个项目都对应一个项目编号(project_number
)、项目名称(project_name
),以及若干个参与该项目的员工;每个员工都对应一个员工编号(employee_number
)、员工姓名(employee_name
)和职位(job_class
);每个职位都对应一个时薪标准(charge_hour
);对参与各个项目的不同员工,该表还记录了相应的工作时长(hours_billed
)。值得注意的是,由于一条项目记录对应了多条员工、时薪及工作时长记录,该表中的某些列(如employee_number
、employee_name
等等)下的单元格含有多个取值,且各取值之间以逗号间隔。
1NF的实现条件及方法
要实现1NF,一张表必须满足以下所有条件:
- 表中不含有重复组(repeating group),即各行/列交叉处的单元格不应含有多个取值 4 ;
- 表中确定了主键。
4 单元格内是否含有需要拆分的多个取值是个容易出现争议的问题,必须结合数据的具体用途决定。
回顾PROJECT_UNF
,不难发现employee_number
、employee_name
、job_class
、charge_hour
和hours_billed
等列下的单元格含有多个取值,所以该表并不符合1NF的第1个条件。要使之达到1NF,需要对其进行以下改造:
- 去除重复组。
employee_number
等列下的单元格含有多个取值,需要将其拆分,使得每个单元格仅包含一个取值。拆分有两种方式5:第一种方式是将一个单元格拆分成多行,其结果如表PROJECT_1NFa
所示( 图 2 上);第二种方式是将一个单元格拆分成多列,其结果如表PROJECT_1NFb
所示( 图 2 下)。 - 确定主键。如果选择上述第一种拆分方式,可以发现
project_number
和employee_number
这两个字段的取值组合能够确定每一行其他字段的取值,因此它们可以作为表的主键;另一方面,选择上述第二种方式拆分单元格后得到的结果表可以使用project_number
作为主键。
5 根据 Silberschatz, Korth, 和 Sudarshan (2019) 提出的建议,在对多值属性(multivalued attribute)进行拆分时,应该选用第一种方式(将一个单元格拆分成多行);在对复合属性(composite attribute)进行拆分时,应该选用第二种方式(将一个单元格拆分成多列)。在本例中,需要拆分的属性均为多值属性,因此最好选择前者,以避免后面将提到的问题。这里把两种方式都拿出来讲只是为了内容的完整性。
尽管从前述定义角度看,表PROJECT_1NFa
和表PROJECT_1NFb
都满足1NF,但在实际应用中通常还是会倾向于采用前者,这是因为后者具有以下几项缺点:1. 需要对原单元格进行拆分后得到的列的个数做出明确限定,显得不够灵活;2. 容易引入空值,造成处理上的不便;3. 当拆分出的列数较多时,不便于进行某些查询(例如每个项目的参与人数);4. 不便于被进一步优化至3NF。考虑到这些因素,下文将仅以表PROJECT_1NFa
为例进行进一步优化。
2NF的实现条件及方法
要实现2NF,一张表必须满足以下所有条件:
- 已实现1NF;
- 表中不存在部分依赖。
我们已经知道,project_number
和employee_number
组成了表PROJECT_1NFa
的一个候选键(该候选键同时也是主键);然而,回顾前文对例表内容描述中所隐含的业务规则,我们会发现表PROJECT_1NFa
中的project_name
仅依赖于project_number
,而employee_name
、job_class
、charge_hour
则仅依赖于employee_number
。这说明该表中存在部分依赖。如果要实现2NF,需要对PROJECT_1NFa
进行以下改造:
- 为每一个涉及到部分依赖的主键属性(primary-key attribute)构建一个新的表,并以该属性作为新表的主键。由于
project_number
和employee_number
均各自涉及到部分依赖,因此需要新增两个表PROJECT_2NF
和EMPLOYEE_2NF
,并分别以project_number
和employee_number
作为它们的主键。 - 将依赖于上述主键属性的列移动到新的表中。由于
project_name
依赖于project_number
,因此需要将project_name
从PROJECT_1NFa
移动到PROJECT_2NF
;同理,还需要把employee_name
、job_class
和charge_hour
从PROJECT_1NFa
移动到EMPLOYEE_2NF
中。在进行移动之后,表PROJECT_1NFa
只剩下project_number
、employee_number
和hours_billed
等三个字段,并满足2NF,我们可以将其重命名为PROJECT_ASSIGNMENT_2NF
。
经上述步骤,我们获得了PROJECT_ASSIGNMENT_2NF
、PROJECT_2NF
和EMPLOYEE_2NF
三个表(见 图 3 ),并且它们均实现了2NF。
3NF的实现条件及方法
要实现3NF,一张表必须满足以下所有条件:
- 满足2NF;
- 表中不存在传递依赖。
观察EMPLOYEE_2NF
,我们会发现charge_hour
实际上并不直接依赖于该表的主键employee_number
,而是直接依赖于非主键属性job_class
(而job_class
则直接依赖于主键employee_number
);这说明该表中存在传递依赖。如果要实现3NF,需要对EMPLOYEE_2NF
进行以下改造:
- 为传递依赖中起决定因素作用的每个非主键属性构建一个新的表,并以该属性作为新表的主键。在表
EMPLOYEE_2NF
中,由于charge_hour
直接依赖于非主键属性job_class
,因此可以构建一个新表JOB_3NF
,并且以job_class
作为该表的主键。 - 将依赖于上述非主键属性的列移动到新的表中。在此例中,需要将
charge_hour
移动到JOB_3NF
。在进行移动之后,EMPLOYEE_2NF
只剩下employee_number
、employee_name
和job_class
字段,并且满足3NF,我们可以将其重命名为EMPLOYEE_3NF
。
另一方面,由于PROJECT_2NF
和PROJECT_ASSIGNMENT_2NF
中的所有非主键属性(non-primary-key attribute)都直接且完全依赖于对应表的主键、不存在传递依赖,所以这两个表事实上已经满足3NF、无需进一步改造;为了反映这一事实,我们可以直接将它们重新命名为PROJECT_3NF
和PROJECT_ASSIGNMENT_3NF
。
经上述步骤,我们获得了PROJECT_ASSIGNMENT_3NF
、PROJECT_3NF
、EMPLOYEE_3NF
和JOB_3NF
四个表(见 图 4 ),并且它们均实现了3NF。
小结
本文主要从实用角度出发,介绍了数据库基本范式的实现条件和方法,以便能够在实际工作中直接应用。另外,本文还在开头简单提及了数据库规范化的意义(如减少数据冗余、提高数据一致性、有利于数据模型的扩展等),但并未就此做出更进一步的介绍;对该主题感兴趣的读者可以参考文末列出的相关文献。
最后,这里再对1NF、2NF和3NF进行一些补充说明:
- 尽管重复组的存在与否是判断一张表是否满足1NF的重要条件,但人们对“重复组”的定义却存在分歧(Date 2019),导致1NF的定义也变得模糊。根据 Connolly 和 Beg (2015) 给出的定义6,本文所示的表
PROJECT_1NFb
并不能被认为是满足1NF的;然而,根据 Date (2019) 的看法7,该表虽然存在不好的设计,却并未违反1NF的条件。 - 本文所使用的例表中,只含有一个候选键,且该候选键就是主键;然而,在现实中,一个表可能含有多个候选键,而部分依赖和传递依赖可能发生在任意候选键上。因此,在判断一个表格是否满足2NF或3NF时,需要针对每个候选键判断是否存在部分依赖和传递依赖;并且,在进行规范化时,也需分别针对各个候选键上的部分依赖和传递依赖进行处理。
6 “A repeating group is an attribute, or group of attributes, within a table that occurs with multiple values for a single occurrence of the nominated key attribute(s) for that table.”
7 “…, a repeating group is not ‘when you repeat the same basic attribute over and over again.’”