前文(数据库设计Step by Step (9)------ER-to-SQL转化)讨论了如何把ER图转化为关系表结构。本文将介绍数据库范式并讨论如何范式化候选表。我们先来看一下此刻处在数据库生命周期中的位置(如下图所示)。

前几篇博文中我们详细的讨论了ER建模的方法。精心设计的ER模型将帮助我们直接得到范式化的表或只需稍许修改即为范式化的表,设计、绘制ER图的重要性也体现在这里。概念数据建模(ER建模)从一开始就潜移默化的引导着我们走向范式化的数据库表结构。
本文的讨论将始于第一范式,止于BCNF范式。在现实数据库设计中,一般需达到的范式化目标是第三或BCNF范式,更高级别的范式更多的是理论价值,本文也不将涉及。

范式基础
关系数据库中的表有时会面对性能、一致性和可维护性方面的问题。举例来说,把整个数据库中的数据都定义在一张表中将导致大量冗余的数据,低效的查询和更新性能,对某些数据的删除将造成有用数据的丢失等。如图1所示,products, salespersons, customers, orders都存储在一张名为Sales的表中。
|------------------|--------------|---------------|---------------|------------|------------|----------------|
| product_name | order_no | cust_name | cust_addr | credit | date | sales_name |
| vacuum cleaner | 1435 | Dave | Austin | 6 | 2010-03-01 | Carl |
| computer | 2730 | Qiang | Plymouth | 10 | 2011-04-15 | Ted |
| refrigerator | 2460 | Mike | Ann Arbor | 8 | 2010-09-12 | Dick |
| DVD player | 519 | Peter | Detroit | 3 | 2010-12-05 | Fred |
| radio | 1986 | Charles | Chicago | 7 | 2011-05-10 | Richard |
| CD player | 1817 | Eric | Mumbai | 8 | 2010-08-03 | Paul |
| vacuum cleaner | 1865 | Charles | Chicago | 7 | 2010-10-01 | Carl |
| vacuum cleaner | 1885 | Betsy | Detroit | 8 | 2009-04-19 | Carl |
| refrigerator | 1943 | Dave | Austin | 6 | 2011-01-04 | Dick |
| television | 2315 | Sakti | East Lansing | 6 | 2011-03-15 | Fred |
(图1 Sales表)
在这张表中,某些产品和客户信息是冗余的,浪费了存储空间。某些查询如"上个月哪些客户订购了吸尘器"需要搜索整张表。当要修改客户Dave的地址需要更新该表的多条记录。最后删除客户Qiang的订单(2730)将造成该客户姓名、地址、信用级别信息的丢失,因为该客户只有唯一这个订单。
如果我们通过一些方法把该大表拆分成多个小表,从而消除上述这些问题使数据库更为高效和可靠,范式化就是为了达到这一目标。范式化是指通过分析表中各属性之间的互相依赖,并把大表映射为多个小表的过程。
第一范式(1NF)
定义:当且仅当一张表的所有列只包含原子值时,即表中每行中的每一个列只有一个值,则该表符合第一范式。
图1中的Sales表的每一行、每一列中只有原子值,故Sales表满足第一范式。
为了更好的理解第一范式,我们讨论一下域、属性、列之间的差异。
域是某属性所有可能值的集合,但同一个域可能被用于多个属性上。举例来说,人名的域包含所有可能的姓名集合,在图1的表中可用于cust_name或sales_name属性。每一列代表一个属性,有些情况下代表不同属性的多个列具有相同的域,这并不会违反第一范式,因为表中的值仍是原子的。
仅符合第一范式的表常会遇到数据重复、更新性能以及更新一致性等问题。为了更好的理解这些问题,我们必须定义键的概念。
超键是一个或多个属性的集合,其能帮助我们唯一确定一条记录。若组成超键属性列的子集仍为一个超键,但该子集少了任何一个属性都将使其不再是一个超键,则该属性列子集称为候选键。主键是从一张表的候选键集合中任意挑选出的,作为该表的一个索引。
作为一个例子,图2中表的所有属性组成一个超键。
|---------------|------------|-------------|---------------|---------------|---------------|-----------------|-----------------|
| report_no | editor | dept_no | dept_name | dept_addr | author_id | author_name | author_addr |
| 4216 | woolf | 15 | design | argus1 | 53 | mantei | cs-tor |
| 4216 | woolf | 15 | design | argus1 | 44 | bolton | mathrev |
| 4216 | woolf | 15 | design | argus1 | 71 | koenig | mathrev |
| 5789 | koenig | 27 | analysis | argus2 | 26 | fry | folkstone |
| 5789 | koenig | 27 | analysis | argus2 | 38 | umar | prise |
| 5789 | koenig | 27 | analysis | argus2 | 71 | koenig | mathrev |
(图2 Report表)
在关系模型中不允许有重复的行,因此一个明显的超键是表的所有列(属性)的组合。假设表中每一个部门的地址(dept_addr)都相同,则除dept_addr之外的属性仍然是一个超键。对其他属性作类似的假设,逐步缩小属性的组合。我们发现属性组合report_no, author_id能唯一确定表中的其他属性,即是一个超键。同时report_no或author_id中的任意一个都无法唯一确定表中的一行,故属性组合report_no, author_id是一个候选键。由于它们是该表的唯一候选键,它们也是该表的主键。
一张表能有多个候选键。举例来说,在图2中,若有附加列author_ssn(SSN:社会保险号),属性组合report_no, author_ssn也能唯一确定表中的其他属性。因此属性组合(report_no, author_id)和(report_no, author_ssn)都是候选键,可以任选其一作为主键。
第二范式(2NF)
为了解释第二以及更高级别范式。我们需引入函数依赖的概念。一个或多个属性值能唯一确定一个或多个其他属性值称为函数依赖。给定某表(R),一组属性(B)函数依赖于另一组属性(A),即在任意时刻每个A值只与唯一的B值相关联。这一函数依赖用A --> B表示。以图2中的表为例,表report的函数依赖如下:
report: report_no --> editor, dept_no
dept_no --> dept_name, dept_addr
author_id --> author_name, author_addr
定义:一张表满足第二范式(2NF)的条件是当且仅当该表满足第一范式且每个非键属性完全依赖于主键。当一个属性出现在函数依赖式的右端,且函数依赖式的左端为表的主键或可由主键传递派生出的属性组,则该属性完全依赖于主键。
report表中一个传递函数依赖的例子:
report_no --> dept_no
dept_no --> dept_name
因为我们能派生出函数依赖(report_no --> dept_name),即dept_name传递依赖于report_no。
继续我们的例子,图2中表的复合键(report_no, author_id)是唯一的候选键,即为表的主键。该表存在一个FD(dept_no --> dept_name, dept_addr),其左端没有主键的任何组成部分。该表的另两个FD(report_no --> editor, dept_no和author_id --> author_name, author_addr)的左端包含主键的一部分但不是全部。故report表的任何一条FD都不满足第二范式的条件。
思考一下仅满足第一范式的report表的缺陷。report_no, editor和dept_no对该Report的每一位author都需要重复,故当Report的editor需要变更时,多条记录必须同步修改。这就是所谓的更新异常(update anomaly),冗余的更新会降低性能。当没有把所有符合条件的记录同步更新时,还会造成数据的不一致。若要在表中加入一位新的author,只有在该author参与了某Report的撰写才能插入该author的记录,这就是所谓的插入异常(insert anomaly)。最后,若某一张Report无效了,所有与该Report相关联的记录必须一起删除。这可能造成author信息的丢失(与该Report相关联的author_id, author_name, author_addr也被删除了)。这一副作用被称为删除异常(delete anomaly),使数据丧失了完整性。
上述这些缺陷可通过把仅满足第一范式的表转化为多张满足第二范式的表来克服。在保留原先函数依赖和语义关系的前提下,把Report表映射为三张小表(report1, report2, report3),其中包含的数据如图3所示。
Report 1
|---------------|------------|-------------|---------------|---------------|
| report_no | editor | dept_no | dept_name | dept_addr |
| 4216 | woolf | 15 | design | argus 1 |
| 5789 | koenig | 27 | analysis | argus 2 |
Report 2
|---------------|-----------------|-----------------|
| author_id | author_name | author_addr |
| 53 | mantei | cs-tor |
| 44 | bolton | mathrev |
| 71 | koenig | mathrev |
| 26 | fry | folkstone |
| 38 | umar | prise |
| 71 | koenig | mathrev |
Report 3
|---------------|---------------|
| report_no | author_id |
| 4216 | 53 |
| 4216 | 44 |
| 4216 | 71 |
| 5789 | 26 |
| 5789 | 38 |
| 5789 | 71 |
(图3 2NF表)
这些满足第二范式表的函数依赖为:
report1: report_no --> editor, dept_no
dept_no --> dept_name, dept_addr
report2: author_id --> author_name, author_addr
report3: report_no, author_id为候选键,无函数依赖
现在我们已得到了三张满足第二范式的表,消除了第一范式表存在的最糟糕的问题。第一、editor, dept_no, dept_name, dept_addr不再需要为每一位author重复。第二、更改一位editor只需要更新report1的一条记录。第三、删除report不再会造成author信息丢失的副作用。
我们可以注意到这三张满足第二范式的表可以直接从ER图转化得到。ER图中的Author、Report实体以及之间的"多对多"关系可根据上一篇博文(数据库设计Step by Step (9)------ER-to-SQL转化)的规则很自然的转化为三张表。
第三范式(3NF)
第二范式相对于第一范式已经有了巨大的进步,但由于存在传递依赖(transitive dependency),满足第二范式的表仍会存在数据操作异常(anomaly)。当一张表中存在传递依赖,其意味着该表中描述了两个单独的事实。每个事实对应于一条函数依赖,函数依赖的左侧各不相同。举例来说,删除一个report,其包含删除report1和report3表中的相应记录(如图3所示),该删除动作的副作用是dept_no, dept_name, dept_addr信息也被删除了。如果把表report1映射为包含列report_no, editor, dept_no的表report11和包含列dept_no, dept_name, dept_addr的表report12(如图4所示),我们就能消除上述问题。
Report11
|---------------|------------|-------------|
| report_no | editor | dept_no |
| 4216 | woolf | 15 |
| 5789 | koenig | 27 |
Report12
|-------------|---------------|---------------|
| dept_no | dept_name | dept_addr |
| 15 | design | argus 1 |
| 27 | analysis | argus 2 |
Report 2
|---------------|-----------------|-----------------|
| author_id | author_name | author_addr |
| 53 | mantei | cs-tor |
| 44 | bolton | mathrev |
| 71 | koenig | mathrev |
| 26 | fry | folkstone |
| 38 | umar | prise |
| 71 | koenig | mathrev |
Report 3
|---------------|---------------|
| report_no | author_id |
| 4216 | 53 |
| 4216 | 44 |
| 4216 | 71 |
| 5789 | 26 |
| 5789 | 38 |
| 5789 | 71 |