Archive for the ‘数据库’ Category

关系型数据库

Sunday, December 3rd, 2006

理论背后的灵感

不运用关系型数据库的理论就想使用关系型数据库管理系统(relational database management system,RDBMS)与不踩离合器就想使用启动标准变速器一样不现实。你不要在歧途上走得更远了。实际上,你的应用程序的问题可能就是起因于一个不合格的数据库。但是情况可能会更糟,你创建的数据库可能会错误报告数据、甚至还会破坏数据。在本系列文章中,你将会学到如何运用关系规则并从中学会如何开发一个可以保护数据合法性的数据库。让我们以回顾关系型数据模型的方式开始本系列文章。
  
在最初……
  
尽管我们已经被新的技术所包围,但我们中的许多人还是得依靠老的成熟技术——RDBMS和关系型数据库理论。它们起源于三十年前,那时保存大量数据的资源是很昂贵的。
  
在我们今天广为使用的关系型数据库出现之前,数据是按扁平文件(flat-file)的格式保存起来的。所有的数据以记录的形式保存到一个大的表格中。设想一下反复输入客户地址、电话号码以及其它重要信息的情形——每一次插入都需要重排客户记录。扁平文件需要冗余的数据,这就导致了大文件的产生。此外,扁平文件数据很难处理,而且需要大量的人力资源去维护它——这最终等效于需要更多的薪水支出、更多的办公空间以及更多的设备。一言以蔽之,关系型数据库理论的产生是由问题所激励出来的,这个问题就是维护数据以及与之相关的程序的花费和复杂度。
  
上述问题的解决方案

关系型模型解决了冗余数据的问题。理论出现之后,工具紧接着也诞生了。在E. F. Codd博士于1970年发表了名为《大规模共享数据银行的关系型模型》的论文(发表在Communications of the ACM杂志1970年6月刊上)之后,解决方案也就出现了。在这篇论文中,Codd博士介绍了用来消除保留冗余数据的必要性的一套规则。这套规则就是关系型数据库理论的起源。
  
由此产生的关系型数据库就成为了按关系表示数据的数据模型。数据模型在概念上是数据、关系以及对数据的约束这三者的集合。关系型数据库模型就是对象、事件和与关系型数据库系统有关的其它事项在概念上的表示。其真正含义就是关系型数据模型要求数据按照关系来保存。
  
关系(一类数据在概念上的集合)由表格(它保存了关于实体的信息)来表示。表中的每一行对应一个tuple(元组),列对应一个属性。在本文中,属性仅仅是关系中的一个命名列。每个属性都和一个值域(该属性可能的赋值范围)相联系。例如,表A给出了给出了一个关系(它保存了关于书的信息)的某些属性的值域。

表A:Book关系的例子

值域定义了哪些值可以保存到特定的栏上。例如,你可以限制name栏的字数等于或者少于35个字符。可接受的名字集合就是值域,该值域可以包括一个上限为35个字符。值域是概念上的;RDBMS并不用除数据类型之外的技术支持值域。你必须使用系统特性,如为了进一步限制栏所接收的数据类型,可以加上对字符数限制的约束。
  
一般只有在进行纯技术讨论时,你才会遇上如关系、tuple和属性这样的术语。在本系列文章的其它部分,我将分别用到以下三个术语:表、记录和栏(这三个术语不太正式,但是得到了广泛使用)。一些人更喜欢用术语:文件、行和字段,但是实际上这是一回事。
  
关系型表格存储关于某一个事情的数据、概念或者事件。表格是保存在行和栏中的相关数据的集合。栏(或者字段)定义为数据的种类。所有的栏组合成一个记录,它表示关于一个条目或者实体的所有相关数据。
  
今天的RDBMS把关系型数据库的理论付诸实现。从技术的角度来说,RDBMS包括了数据以及管理这些数据的接口工具。这意味着使用这个系统你就不需要其它工具来添加、删除、更新以及观看数据。你所需要的所有东西这个系统都有。这个系统还包括元数据(metadata):对保存数据本身的描述。
  
为什么使用关系型数据库

关系型理论在建立时没有考虑到存储器——无论是挥发性的还是永久性的——是非常昂贵而且计算机需要许多物理空间的问题。显然,这些问题已经不复存在了,所以使用关系型数据库还会存在什么问题呢?利用关系型数据模型,你可以做到:
  
消除冗余数据。
消除数据间的不一致性。
保护数据的完整性。

由于存储器很便宜了,从存储的观点来看,数据库是否存在冗余数据并不是一件大不了的事情。然而,减少输入条目所花费的时间附带出来的一个好处就是减少数据输入错误。每个条目只保存一次,所以即使你犯了一个错误,你也只需要修正出错的那个输入即可。关系型数据模型相对其它数据模型的其它个好处将在本系列文章的后续部分陆续介绍。
  
第一步——了解你的数据

在Codd博士的论文中,你找不到这个规则,但是熟悉数据并对数据库的目的有个彻底的了解是创建高效率、灵活的数据库的最重要的步骤。你需要和用户以及管理员交谈,收集所有的正在使用以及已发行的书面报表和报告。用户知道数据库需要收集什么数据以及如何处理。如果有必要,你要和用户坐在一起处理数据并观察他们的手工处理数据方法。管理员知道他们希望数据是以何种方式表示出来。用户和管理员这俩方面的观点都很重要,所以都要听取。
  
在会见用户和管理者之后,写一个任务称述。称述的长短完全根据需要而定,但是它应该清楚的定义数据库的目标并阐明实现该目标的主要过程,主要过程不需要写的很具体。然后把任务称述共享给参与项目的所有开发成员以及用户和管理员,以确保你知道项目的症结之所在。当要求你修订任务称述时,你不必感到惊讶或者心烦。修订的次数越多,你对产生误解的机会也就越少,这就减少了日后修正带来的麻烦。
  
使用范式创建数据库

关系型数据库理论可能是20世纪60年代和70年代存储系统先锋的救星,但是从那是开始它就成了许多数据开发人员的毒药,就是因为现代数据库系统发展得如此之好,以至于它将其关系型支柱对开发人员隐藏了。设计良好的关系型数据库很容易使用、很灵活,并且能够保护数据的有效性。而设计不良的数据相反仍然能够发挥相当的作用,但是最终可能会导致数据的无效、错误或者丢失。
  
开发人员有一些专用的规则,叫做范式(normal forms),他们根据这些规则来创建设计良好的数据库。在这里,我将通过创建一个用于保存书籍信息的简单数据库来探讨一下范式。
  
确定实体和元素
  
设计数据库的第一步是做你的家庭作业并确定你所需要的实体。实体是数据一种类型的概念集。通常只从一两个实体开始,再随着你数据的规范化而增加列表。对于我们的示例数据库,它看上去就好像我们只需要一个实体——书。
  
在确定了所需要实体的清单之后,你下一步就需要为每个实体创建数据元素(也就是说,你需要保存的信息)的清单。收集这样的信息有多种途径,但是最有效的可能就是依赖你的用户了。向你的用户询问他们日常工作的情况,要求查看当前完成他们工作所需要的各种表格和报告。例如,订单上可能会列出你创建销售应用程序所需要的许多数据元素。
  
我们的书籍实体没有书面表格和报告可用,但是下列元素清单将有助于我们开始设计这个数据库:
  
{Title, Author, ISBN, Price, Publisher, Category}
  
很重要的一点是,要注意,把我们这里要用的实体移动到元素的过程并不能适用于所有状况。你所需要的实体不会总是像我们书籍示例那样清楚,所以你可能要从数据元素的一长串清单开始,在后面你会根据实体来划分元素。

正规化的头几步
  
一旦有了实体清单(表格)和数据元素(字段),你就准备好让关系型数据库理论运作了。这个理论的主要推动力是规范化——删除任何重复的组和冗余的数据,并把它们放到两个或者更多相关表里的过程。你并不是一定需要拥有一个以上的表格,但是你的数据简单到只需要一个表格的机会并不多。
  
你应该小心地检查数据(这些数据会出现在多条记录里)和依赖性错误的实体和元素清单,并把已损坏的字段移动到不同的表格里。例如,你可能列出同一个作者的多本书,并在数据库里重复了作者的名字。当你认为会一次又一次地看到相同的数据值时,你就应该考虑把这个字段移动到另一个表格里了。
  
要记住,在这一点上,你只是在操作潜在表格的列表,而不应该真正地创建这个表格:现在还是要用笔和纸来列表。
  
范式简介
  
数据库规范化的过程非常著名,所以有正式的规则来保证规范化数据库的建设。这些规则有七条,叫做范式,而在大多数情况下头四条就够用了:
  
第一范式(1NF)——这条规则有几个要求,包括:无多值项目(multivalued item)和重复组(repeating group);每个字段都是原子型的(atomic),也就是说每个字段必须包含可能的最小数据元素;以及表格含有关键字(key)。

第二范式(2NF)——表格必须按照1NF来规范化。所有的字段必须引用(或者描述)主键值。如果主键基于一个以上的字段,那么每个nonkey字段必须取决于复杂键(complex key),而不仅仅是一个没有键的字段。不支持主键的nonkey字段应该被移动到另一个表格里去。

第三范式(3NF)——表格必须符合1NF和2NF的要求。所有的字段都必须相互独立。任何描述nonkey字段的字段都必须被移动到另一个表格里。

Boyce-Codd范式(BCNF)——一定不能存在依赖于nonkey的字段。这条规则实际上是3NF的一个子规则,用于捕捉可能会通过进程的依赖性。这一点相当的抽象,一开始是很难应用的。
  
以上的规则很精确,但是技术定义以及规范化的规则能够被简化成下面几点:
  
每个字段必须尽量小。
每个字段只能包含一个数据项目。
每条记录都必须是唯一的。
注意重复的条目。
每个字段都必须完全支持主键,而且只支持主键。
下一步该做什么?
  
应用这些规范化规则,尤其是1NF的几个要求,将会是个很需要技巧的过程。正如你会在下面内容里看到的,我会开始真正地把范式应用到实力数据库上,在进行了其他规范化的步骤之后,你就会重新回到1NF。

应用第一范式

关系型数据库的理论最早可以追溯到E. F. Codd博士1970年的论文《大型共享数据库的数据关系模型》,在这篇文章里,他总结出了七条抽象的规则,叫做范式(normal form),用来帮助创建设计良好的数据库。
这七条规则的前四条——第一范式(First Normal Form,1NF)、第二范式(2NF)、第三范式(3NF)和Boyce-Codd范式(BCNF)——在大多数情况下已经够用了。
  
这些范式是非常抽象的,以至于有些开发人员在如何应用它们上存在问题。也许理解范式最好的方式是开始将它们应用于数据,因为规则在你确实有数据要划分的时候才更有作用。在本文中,我会对一个书目示例数据库应用1NF的规则,这些规则在一开始应用的时候是最复杂的。
  
你会回忆起,1NF的要求是:
  
多值字段(multivalued field)必须要被移动到另一个表格里。
每个字段必须是原子型的(atomic),或者说要尽量地小。
每个字段都必须有一个关键字(key).
重复的值必须要被移动到另一个表格里。
  
我将要使用的简单表格是用来保存一些书目信息的。到目前为止,这个Books表格有下面这些字段:
  
{Title, Author, ISBN, Price, Publisher, Category}

将多值字段移动到另一个表格
  
应用1NF的第一步是确保表格没有包含多值字段,从定义可知它能够保存一个以上可能的条目。我们最开始的清单有两个可能会违反这一规则的地方: Author(作者)和Category(分类)。许多书都有多个作者,所以Author字段就会出问题。类似的,一本书可以被归入多个类别。例如《金银岛(Treasure Island)》可以被归为儿童读物、冒险类、经典类,以及其他类等等。
  
更正这个问题的唯一方法是把这些违反规则的字段转移到另一个表格里。你可能会发现字段在另一个已存在表格里工作得会更好,但是这样的情况非常少见。在大多数情况下,你需要为你所移动的每个字段都创建新的表格。为了满足1NF的要求,我为Author和Category这两个字段创建了两个新的表格,其他的字段都留在Books表格里没有动:
  
{Title, ISBN, Price, Publisher}
  
每个字段都必须是原子型的

1NF的下一项要求是说,每个字段都必须是原子型的,这就表示每个字段必须保存可能会有的最小数据元素。这条规则有助于搜索和排序。Author字段在这里再一次出现了问题,因为一个名字可以被分成多条信息。我们需要一个字段用于姓,另一个字段用于名,这会让搜索作者姓名变得容易得多。
  
在这一点上,我会更进一步把Authors表格分成至少两个字段:FirstName(名)LastName(姓),那么我数据库的布局就是下面这样的:
  
Books: {Title, ISBN, Price, Publisher}
Authors: {FirstName, LastName}
Categories: {Category}
  
每个字段都必须有一个关键字
  
搜索有重复记录(duplicate record)的表格是很难的;事实上,关系模型不允许表格包含有重复记录。所以,一个表格里字段或者列的值必须是唯一的。唯一性可以通过检查key(关键字)来确定,关键字可以由一个单列或者列的组合构成,这样的列叫做composite key(复合关键字)。
  
关键字有很多不同的类型:
  
超关键字(Super key):唯一辨别表格里记录的一个列或者一组列。
备选关键字(Candidate key):包含有确定唯一性所需要的最少列的超关键字。
主关键字(Primary key):用来唯一辨别表格里记录的备选关键字。
备用关键字(Alternate key):没有被选为主关键字的备选键。
外来关键字(Foreign key):表格内匹配同一表格或者另一表格里备选关键字的一个列或者一组列。外来键允许你将一个表格里的记录和另一个表格里的数据相关联。
  
这里列出来的关键字的类型并不是相互排斥的;一个关键字可以同时被归入多个类。从定义上说,每个表格必须至少有一个主关键字。
  
要确定我们示例表格的主关键字,就让我们从找到超关键字开始。Authors和Categories表格的超关键字很容易找到,因为它们的字段非常少,但是Books表格的会稍稍困难一点。尽管两本书有同一个名字的机会几乎没有,但是这不是不可能的,所以我不能把Title(书名)作为Books表格的关键字。两本书来自同一个出版社具有同一个名字或者具有同一个ISBN的机会应该更小,所以我可以从这些可能性中创建一些超关键字。表A列出了这三个表格的所有超关键字:

表B:示例数据库的超关键字

  
Books表格事实上有更多的超关键字,例如Title、ISBN和Publisher,但是要包含所有这些关键字就又太多了。
  
找到备选关键字
  
现在是缩短超关键字列表来找到备选关键字的时候了,这些备选关键字包含有一个字段里满足唯一性所需的最少列。Categories表格在这一点上不存在问题,因为它只有一个字段。Authors表格只有一个超关键字,所以很明显它就是备选关键字。
  
但是,Books表格有点麻烦,但是在最终的分析里,ISBN字段是备选关键字的最好选择。ISBN字段应该是唯一的,但是由于这些数字是由出版商来指定的,所以还是可能出现使用同一ISBN的两本书。实际情况是,只使用ISBN可能永远也不会碰到问题,但是一旦出现这样的小错误,你的整个数据库可能都会崩溃。因此,我决定把Title和ISBN这两个复合超关键字作为Books表格的备选关键字。
  
确定主关键字
  
主关键字只不过是你最后用来唯一辨别表格里每条记录的备选关键字。在做完这些事之后,为每个表格分配主关键字就很容易了。现在我为每个示例数据库定义了下列表格(星号表示主关键字字段):
  
Books: {*Title, *ISBN, Price, Publisher}
Authors: {*FirstName, *LastName}
Categories: {*Category, Description}
  
要注意,Categories包含有一个新的字段——Description。单字段的表格是可以接受的,但是加入了描述文本的字段将有助于更加完全地解释每个分类。
  
———————————————–
  
将计数字段(counter field)作为主关键字

大多数关系型数据库管理系统(RDBMS)都会提供一类计数或者自动编号的数据类型,它会为每条记录分配一个连续的数值。尽管这些计数字段会保证你得到一个备选关键字,但是这个关键字对于搜索是毫无疑义的。如果你把计数字段作为主关键字,你至少还需要另一个字段,这样才能以有意义的方式找到记录。
  
——————————————-
  
那么Authors又会是什么样的呢?在Authors表格里出现重复的姓和名的机会好像不多,尤其当你的数据库很小的时候。但是不管怎么说这不是不可能的。你可以为这个表格分配一个计数字段,这肯定会解决唯一性的问题,但是这不会有助于你区别不同的作者。
  
在这里,解决我们问题最简单的方法是加入某种联系信息,例如电子邮件地址或者作者所居住的州或者地区等信息,但是这还是不够明确。你有可能碰到住在同一个州同名同姓的两个作者。这样的情况不多但不是没有可能,所以最好还是为出错做好准备。你可以考虑加入每个作者的邮件地址,但是为了保持例子的简单,我只加入了州名和邮政编码,并扩展主关键字,让其能够同时包含两个新的字段:
  
Authors: {*FirstName, *LastName, *State, *ZIP}
  
在这一点上,这真的是保证Authors表格唯一性的唯一方法。
  
删除重复值
  
1NF需要我们满足的最后一条要求是在数据里不能有重复的组(group)。在没有检查真实数据之前,要确定你是否满足了这一要求是相当困难的,但是既然我们已经碰到了,不管怎么样就应该着手解决。之后,如果看到一个值重复了多次,我就会考虑把这个字段移动到一个新的表格里。如果我数据库里的表格通过别的途径被正确地规范化了,那么重新建模将不会是个大问题。事实上,许多数据库应用程序似乎总在不停地扩展
  
这最后一条规则最明显的违反者是Books表格里的Publisher(出版商)字段。尽管有很多出版商,但是我毫无疑问会发现自己一次又一次地碰到同一个出版商。为了让Books满足1NF,我必须要把这个字段移动到另一个表格里。现在示例数据库看起来像下面这样:
  
Books: {*Title, *ISBN, Price}
Authors: {*FirstName, *LastName, *State, *ZIP}
Categories: {*Category, Description}
Publishers: {*Publisher}
  
你可能不会碰到重复的出版商名,但是为了安全起见,你可以决定将自动编号字段作为主关键字。如果这样做的话,你的表格设计应该会像下面这样:
  
Publishers: {*PublisherID, Name}
  
最后,示例数据库里的表格看上去都符合1NF的要求了。每个字段都尽可能的小,没有重复的组和多值字段,每个表格都有一个关键字。你可能会问还差什么?毕竟,开始的时候只有一个表格。

实现正规化

设计阶段,花在数据正规化上的时间可能比花上其他任何任务上的时间都要多。而且数据越多,这个过程所花的时间更长。根据以往的经验,你可能发现最困难的就是满足第一范式(1NF)的所有要求,因为将重复的值移动到另一个表时,经常会消除不恰当的依赖。
  
完成最困难的部分后,你可能选择在1NF之后就停止了,但不要这样做。请继续对数据进行正规化,尽可能地通过第二范式(2NF),第三范式(3NF),甚至通过Boyce-Codd范式(BCNF)。这样就能找出那些具有依赖性的数据元素,否则它们会在设计过程中悄悄地溜走,并在以后造成问题。最好在设计期间就发现这些问题——不要等到用户发现自己的工作无法完成,或者等到你开始损失金钱的时候。
  
在该系列的上一篇文章中,我们已经从一个表着手,并对其进行了处理,使其符合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}
  
表的数量虽然在快速增加,但请不要担心。事实上,我们尚未完工。在本系列的下一篇文章中,甚至可能出现更多的表。届时,我们将讨论主键和外键字段,并解释如何用它们在多个表之间建立关系。

定义数据库表格之间的关系

设计关系型数据库的重头戏是把数据元素分别放进相关的表格里。一旦准备好开始操作数据,你就要依靠表格之间的关系把数据以有意义的方式联系到一起。例如,(单独的)订单信息是没有用的,除非你知道是哪个用户下了订单。

到现在这个时候,你可能已经意识到了,你没有把客户信息和订单信息保存在同一个表格里。但是,你把订单信息和客户数据保存在两个相关的表格里,然后使用这两个表格之间的关系来同时查看每个订单及其相对应的客户信息。如果说规范化的表格是关系型数据库的基础,那么关系就是其基石。

出发点
  
下面这些数据要用在本文的示例中。使用Boyce-Codd范式(BCNF)来规范化数据的过程产生了七个关系表:

Books: {Title*, ISBN, Price}
Authors: {FirstName*, LastName*}
ZIPCodes: {ZIPCode*}
Categories: {Category*, Description}
Publishers: {Publisher*}
States: {State*}
Cities: {City*}
  
现在是该说明这些表格之间是如何建立关联的时候了。
  
关系的类型
  
你和你的家庭成员之间存在着多种关系。例如,你和你母亲就是相关的。你只有一个母亲,但是她可以有多个子女。你和你的兄弟姐妹是相关的——你可以有很多的兄弟和姐妹,当然,他们也有很多兄弟和姐妹。如果你结了婚,你和你的配偶就会有一个配偶——这是相互的——但是一次只有一个。数据库的关系非常类似:它们是通过表格相关联的。有三种类型的关系:
  
一对一(One-to-one):在关系的每一边,这两个表格都只有一条记录。每个关键字的值都只和关系表里的一条记录(或者没有记录)相关。它们就是一对配偶一样——你可以结婚,也可以不结婚,但是如果结了婚,你和你的配偶就只能有一个配偶。大多数一对一的关系都是商业规则所要求的,而不是源自于数据的要求。在没这样商业规则限制的时候,你通常可以把两个表格合并进一个表格,而且不会打破任何规范化的规则。

一对多(One-to-many):主关键字表格只包含有一条记录,这条记录和关系表里的无记录、一条记录或者多条记录相关。这种关系同你和你父母之间的关系很类似。你只有一个母亲,但是你的母亲可以有多个子女。

多对多(Many-to-many):两个表格里的每条记录都可以和另一个表格里任意数量的记录(或者无记录)相关。例如,如果你有好几个兄弟姐妹,那么你的兄弟姐妹也有好几个兄弟姐妹。多对多的关系需要引入第三个表格,叫做联系表(associate table或者linking table)因为关系型系统不能直接实现这种关系。

建立关系
  
在考虑建立关系表之间的关系之前,你可能需要非常熟悉数据。只有在熟悉数据之后,关联会比你刚开始的时候更明显。你的数据库系统要依靠匹配两个表格里的值来建立关系。如果匹配的话,系统会从这两个表格里抽出数据来创建一个虚拟记录。例如,你可能想要查看某个作者写的所有书。在本文里,系统会匹配Books和Authors这两个表格里的值。要记住在大多数时候,所产生的记录是动态的,这就意味着对这条虚拟记录的任何改动通常都会作用到底层的表格上,这一点非常重要。
  
这些进行匹配的值都是主关键字值和外来关键字值。(关系模型并不要求关系要根据主关键字类来确定。你可以使用表格里的备选关键字,但是使用主关键字是大家认可的标准。)在(本系列的)第二篇文章里你已经了解了主关键字——主关键字会唯一辨别表格里的每条记录。外来关键字简单地说就是一个表格在另一个表格里的主关键字。这样看来,你要做的东西不多——只用简单地把主关键字作为外来关键字添加到关系表里就行了。
  
唯一需要注意的是,外来关键字字段的数据类型必须和主关键字的相同。但是有些系统可以允许这条规则的一个例外,它们能够允许数字和自动编号(autonumbering)字段(例如SQL服务器Identity的Access的AutoNumber)建立关系。此外,外来关键字的值可以是空(Null),尽管推荐的是:在没有特别原因的情况下,不要让外来关键字为空。你有可能永远都不会使用需要这项功能的数据库。
  
现在回到你的示例数据库,并开始正确地输入外来关键字。(请继续在纸上打草稿——在你的数据库系统里真正创建表格仍然是件很容易的事情。在纸上纠正错误要容易得多。)要记住,你正在把主关键字的值添加到关系表里。只用调用条目之间的关系就行了,而其他的就简单了:
  
书(book)和分类(category)相关。
书和出版商(publisher)相关。
书和作者(author)相关。
作者和邮政编码(ZIP code)相关。
邮政编码和城市(city)相关。
城市和州(state)相关。
  
这一步不是一成不变的,你可能会发现在规范化的过程中加入外来关键字会更容易一些。在把字段移动到一个新的表格时,你可能要把这个新表格的主关键字添加到原来的表格里,作为其为外来关键字。但是,在你继续规范化剩余数据的时候,外来键常常会发生改变。你会发现在所有这些表格被全部规范化之后,一次添加所有的外来关键字,这样的效率会更高。
  
现在让我们一次操作一个表格,就从Books表格开始,它在这个时候只有三个字段。很明显,Authors、Categories和Publishers表格的主关键字会被添加到Books里。当你完成的时候,Books表格就有了七个字段:

Books
Title (PK)
ISBN (PK)
Price
FirstNameFK (FK) Authors.FirstName many-to-many
LastNameFK (FK) Authors.LastName many-to-many
CategoryFK (FK) Categories.Category many-to-many
PublisherFK (FK) Publishers.Publisher one-to-many

要记住,Authors表格里的主关键字是一个基于姓和名字段的复合关键字。所以你必须要把这个两个字段都添加到Books表格里。要注意,外来关键字字段名的结尾包含有FK这个后缀。加入这个后缀有助于提高可读性和自我归档。通过名称这种方式来区别外来关键字会让追踪它们更简单。如果主关键字和外来关键字的名称不同,这没有关系。
  
这里出现了三种关系:Books和Authors、Books和Categories,以及Books和Publishers。这三种关系中的两种所存在的问题可能没有那么明显:
  
Books和Authors之间的关系:一本书可以有多个作者。

Books和Categories之间的关系:一本书可以被归入多个类。
   
这两者的关系是多对多的关系。先前我告诉过你,表格不能直接实现这样的关系,而需要第三个联系表来实现。(Books和Publishers的关系是一对多的关系,就像现在所说的这样是没有问题的。)
  
这两个刚发现的多对多关系将需要一个联系表来包含来自每个表格的主关键字,并将其作为外来关键字。新的联系表是:

Books Author smmlink
TitleFK (FK) Books.Title one-to-many
ISBNFK (FK) Books.ISBN one-to-many
FirstNameFK (FK) Authors.FirstName one-to-many
LastNameFK (FK) Authors.LastName one-to-many
  
Books Categories mmlink
TitleFK (FK) Books.Title one-to-many
ISBNFK (FK) Books.ISBN one-to-many
CategoryFK (FK) Categories.Category one-to-many
  
没有必要更改Categories、Authors或者Publishers表格。但是,你必须把FirstNameFK、LastNameFK和CategoryFK这三个外来关键字从Books里移走:

Books
Title (PK)
ISBN (PK)
Price
PublisherFK (FK) Publishers.Publisher one-to-many
  
现在,让我们转到Authors表格上来,它现在有两个字段。每个作者都和ZIPCodes表格里邮政编码的值相关。但是,每个邮政编码会和多个作者相关。要实现这种一对多的关系,就要把ZIPCodes表格的主关键字添加进Authors表格作为外来关键字:

Authors
FirstName (PK)
LastName (PK)
ZIPCodeFK (FK) ZIPCodes.ZIPCode one-to-many
  
至此,你已经准备好了处理剩下的地址部分了。看到它们被分在不同的表格里是很让人奇怪的,但是这是遵照BCNF正确规范化数据的结果。每个邮政编码的值只会有一个对应的城市值和州值。每个城市和州的值只会被输入进其对应的表格里一次。ZIPCodes和Cities表格需要外来关键字字段来实现这些关系:

ZIPCodes
ZIPCode (PK)
CityFK (FK) Cities.City one-to-many
  
Cities
City (PK)
StateFK (FK) States.State one-to-many
  
States
State (PK)
  
从一个到九个
  
最后,你有了九个表格:Books、Authors、Categories、Publishers、ZIPCodes、Cities、States、 BooksAuthorsmmlink和BooksCategoriesmmlink。图A是这个示例表格数据库最终的图形形式。很难想像一个简单的数据表格会被分成九个表格。
  
图C:原来的表格现在需要九个表格

由于这个示例数据库很简单,你可能会问这些关系有什么作用。看起来你仍在保存冗余的数据,只不过形式不同罢了——通过外来键来实现。这是因为我们的表格现在只有很少几个字段。试想一下有十几个字段的表格。需要承认的是,你仍然需要把表格的主关键字作为外来关键字保存进关系表里,但是至多可能最多增加一到两个字段。比较一下为这个表格里的每一条记录都添加十几个条目的情形吧。

转自BUILDER.COM

Wordpress数据库中的表、字段、类型及说明

Friday, November 10th, 2006