设计阶段,花在数据正规化上的时间可能比花上其他任何任务上的时间都要多。而且数据越多,这个过程所花的时间更长。根据以往的经验,你可能发现最困难的就是满足第一范式(1NF)的所有要求,因为将重复的值移动到另一个表时,经常会消除不恰当的依赖。
完成最困难的部分后,你可能选择在1NF之后就停止了,但不要这样做。请继续对数据进行正规化,尽可能地通过第二范式(2NF),第三范式(3NF),甚至通过Boyce-Codd范式(BCNF)。这样就能找出那些具有依赖性的数据元素,否则它们会在设计过程中悄悄地溜走,并在以后造成问题。最好在设计期间就发现这些问题——不要等到用户发现自己的工作无法完成,或者等到你开始损失金钱的时候。
系列文章简介
你现在正在阅读的是Builder.com Relational Database Design系列中的一篇文章。如果错过了前3篇文章,请首先阅读它们:
《关系型数据库:理论背后的灵感》
《关系型数据库:使用范式创建数据库》
《关系型数据库:应用第一范式》
在该系列的上一篇文章中,我们已经从一个表着手,并对其进行了处理,使其符合1NF的要求。该表最终变成了4个表。现在,让我们通过应用2NF、3NF和BCNF来完成正规化过程。
继续未完的工作
我们的示范数据库在完工以后,将用来存储和书籍有关的数据;这是一个非常简单的目的,所以只需要一个简单的数据库。我们现在已经有4个表,而且全部正规化为1NF(记住,关键字段是用星号表示的):
Books: {*Title, *ISBN, Price}
Authors: {*FirstName, *LastName, *State, *ZIP}
Categories: {*Category, Description}
Publishers: {*Publisher}
应用2NF
为了满足2NF的要求,表首先必须正规化成1NF,也就是其中没有多值项,没有重复的组,每个字段都只能包含原子值,而且每个表都必须包含一个键。迄今为止,似乎所有表都满足这个要求。2NF的第二个要求是所有字段(在设计阶段通常称为“属性”)都必须依赖于主键,而且只能依赖于主键。就目前来看,似乎所有属性都满足2NF的要求,无需采取进一步的操作。
另一方面,假定Books表中还存储着用于描述借阅者的大量属性。有的属性不会违反2NF的要求,例如Books表中的一个lent date(借阅日期)属性。然而,其他数据(比如借阅者的姓名、地址等等)就会违反2NF,因为和借阅有关的信息不能完全地支持或描述书籍本身。
应用3NF
一个表在完成了2NF正规化后,可开始检查它是否违反3NF。3NF要求所有字段都相互独立。任何字段如果依赖于一个非关键字段,都必须转移到另一个表中。为了找出违反3NF的地方,最简单的方式就是修改每个属性的值,看它是否立即使其他属性所包含的数据无效。这种简单测试虽然不能找出违反3NF的所有地方,但却是一个不错的开端。
Authors表存在一个可能违反3NF的地方:如果更改State值,那么可能同时还要更新ZIP;反之亦然。例如,假如作者移居另外一个州,那么上述两个值都需要修改。为了避免这种形式的依赖性,你需要将State属性转移到一个新表中,如下所示:
Authors: {*FirstName, *LastName, *ZIP}
States: {*State}
上述修改的结果就是,每个作者都有了一个ZIP值,其中部分值可能重复,但在States表中,每个州只占用一条记录。如果某个作者移居到其他某个州,你虽然需要更新ZIP值,但只需将记录与一个不同的州联系起来就可以了。如果是一个新出现的州,就可能需要输入一个新的州值,但至少州值不会重复。
就目前来说,感觉是在创建一个查找表(lookup table)。以后,这些表会通过它们的主键和外键值相互联系,但在正式建立联系之前,按上述逻辑进行操作可能显得比较困难。不过,如果搞不清楚当前的状况,请不要担心。目前只需将注意力集中在规则上就可以了。
现在已经有了5个表,全部正规化成3NF:
Books: {*Title, *ISBN, Price}
Authors: {*FirstName, *LastName, *ZIP}
Categories: {*Category, Description}
Publishers: {*Publisher}
States: {*State}
有人会对此产生疑问,因为有一个属性似乎没有考虑到,也就是 Authors表中的ZIP。前面说过,ZIP值是有可能重复的。在这个简单的应用程序中,将ZIP留在Authors表中似乎是可以接受的;无论如何,数据库都应该能高效地运行。不过,这个表并没有充分地正规化,所以下面尝试将ZIP转移到一个新表中。在移动了ZIP之后,我们就有了6个表:
Books: {*Title, *ISBN, Price}
Authors: {*FirstName, *LastName}
ZIPCodes: {*ZIP}
Categories: {*Category, Description}
Publishers: {*Publisher}
States: {*State}
正规化不一定能保证效率
并非每个表都必须在完全正规化后才能获得高效率。换言之,如果你发现能使数据库变得更高效,那么完全可以对一个表进行反正规化处理。
应用BCNF
BCNF本质上是3NF的一个子规则,许多开发者认为它完全没有必要,所以在3NF之后就停止正规化了。有人认为如果强制遵循这一规则,反而会降低性能。但对于目前通常都很强大的系统来说,性能恐怕不是一个大问题,除非你试图操纵数百万条记录。当然,你不一定非要包括BCNF,必须权衡在进行了BCNF正规化之后,对性能造成的影响是否值得一个完全正规化的数据库在灵活性上的好处。
BCNF要求任何属性都绝对没有机会依赖一个非关键字段。就目前来说,我们的表似乎能满足这一要求。所以,让我们在States表中添加一个City属性,使局面变得复杂一些:
States: {*City, *State}
每个城市和州记录都是惟一的,而且与一个ZIP值相关。但是,州值现在展示了一个重复的组,因为每个州都可能有多个城市。解决方案是将City属性转移到它自己的表中,如下所示:
Cities: {*City}
States: {*State}
虽然City和State属性是一种不恰当的依赖,但实际是因为存在重复的值,所以才需要新建Cities表。这种问题通常在强制1NF时就能捕捉,但只有通过强制BCNF,才能最终完全捕捉到造成重复值的错误。通常,虽然一个依赖性问题会暂时迷惑你的眼睛,但只有在强迫了BCNF之后,才能彻底消除依赖非关键字段的问题。
对最初通过BCNF创建的表进行了正规化之后,表的总数就增加到了7个:
Books: {*Title, *ISBN, Price}
Authors: {*FirstName, *LastName}
ZIPCodes: {*ZIP}
Categories: {*Category, Description}
Publishers: {*Publisher}
States: {*State}
Cities: {*City}
表的数量虽然在快速增加,但请不要担心。事实上,我们尚未完工。在本系列的下一篇文章中,甚至可能出现更多的表。届时,我们将讨论主键和外键字段,并解释如何用它们在多个表之间建立关系。