关于多值依赖--范式!

原文
[url]http://book.csdn.net/bookfiles/1168/100116834872.shtml[/url]
.6 实体中的多值依赖

在本节中,我们会看看下一个层次的规范化。虽然在考虑了实现规范化上所需投入的时间,以及最终数据库的性能开销后,这个层次的规范化并没有被普遍采用,然而,本节和这之前的部分一样绝对重要。

在前面的部分中,我们处理的是属性的结构以及非键属性和键之间的关系。接下来将要讨论的两个范式处理的仍然是非键属性间的关系,但是,现在我们处理的是关系的基数,以及当基数大于一时可能引发的各种问题。

虽然第三范式通常被认为是正确的数据库设计的巅峰,然而,在逻辑设计中,仍然可能留下了一些严重的问题。说得更具体些,本节的范式要处理的是属性之间的多值关系和依赖情况。为此,我们将首先较详细地研究第四范式,然后简单地介绍第五范式。

4.6.1 第四范式

到目前为止,我们的规范化原则处理的都是实体中列之间的冗余。我们没有解决的问题是:如果实体有组合键,而键中的一列或多列仍然持有冗余数据,则会引发若干问题。实体规范化的第四范式讲的就是这些问题。在简单情况下,朝着第四范式前进要解决的是类似于这样的问题:建立模型,将实际情况下需要多值的属性存储为单一值。要解决的第二类问题更加不易把握,因为它处理的是三元关系,以及如何将其分解为更易处理的、更小的表。

为了便于说明,考虑一个将学生分配到班级的实体,同时,我们想要表示每个班级每个学生的老师。因此,我们将老师和学生放到如图4-26所示的ClassAssignment实体中。

这样做看起来很不错,并且,由于展示ClassAssignment实例需要所有的属性,这可以说是个最优的解决方案。然而,进一步的考虑发现这个实体并不让人满意。如果我们现在想要改变一个班级的老师,将不得不对班级中的所有学生实例进行修改,因为每个ClassAssignment实例都包括了班级和老师。

很明显,这样做不是最好的,因为规范化最主要的一点就是消除冗余信息,尤其是消除修改一条信息需要在若干地方修改这条信息的情况。注意到一个班级有若干学生,一名学生可以在若干个班级中,以及共有若干位老师这个事实,可以找到一个简单的解决方案。(我还将一步步地展示一个讲同样问题的例子,教会你如何在不是立刻就有思路的情况下寻找到解决方案。)

为了实现新的解决方案,我们创建3张表:一张针对班级,其他两张分别体现老师与班级的关系以及学生与班级的关系。如图4-27所示显示了修改后的设计。

[img]http://dl.iteye.com/upload/picture/pic/64041/e6967896-f0ed-30e2-be0a-ff0bfe683f3c.jpg[/img]

图4-27 修改后的班级模型

之所以可以这样变形,关键在于学生和老师之间的关系(至少就班级分配来说)是完全围绕着班级进行的。因此,我们不需要体现Student和Teacher实体之间的关系。通过Class实体进行联结,可以得到有关哪个学生由哪个老师授的信息。

这类变形在很大程度上是第四范式要做的事。我们必须打破这种三元关系,将其变为没有冗余信息的更有用的形式。

实体要满足第四范式,必须满足下列条件。

l 实体必须满足BCNF:该条件保证所有键都被恰当地定义了,并且,实体中所有的值都正确地依赖于实体的键。

l 在一个属性与实体的键之间,多值依赖(MVD)必须不能超过一个:能够存储多值,并且与实体的键相关联的属性不能超过一个,否则会出现重复的数据。另外,我们应该保证,对多值属性的每个值,都不会在单值属性中重复它。

看一些例子会有助于弄清楚以上的想法。让我们先看看违反第四范式主要的3种形式:

l 三元关系;

l 潜伏的多值属性;

l 临时数据或历史值。

从我自己的观点来看,理解第四范式非常关键。要遵守第四范式的方法也相当简单。认为第三范式之后的范式没有意义的观点中,存在着一些重大的误解。一旦你明白了在第四范式的级别上规范化的意义,你就再也不会忽略第四范式所代表的内容。

1. 三元关系

在第1章我们简单地看过三元关系。通常,在真实环境下,关系不会表现为简单的二元类型,三元甚至三元以上的关系都很常见。只要我们在键中的任何地方看到了3个(或更多)识别性的或强制的非识别性关系,我们就有可能碰到麻烦(同时,要考虑到三元关系也许是挑选的键不够好造成的)。

考虑我们设计出一套实体来支持会议安排的情况,我们存储了有关会议、发言人以及举行会议所在的房间的信息。

让我们假设需要遵循如下的业务规则集合:

l 一场演讲可以安排多位发言人;

l 一场演讲可以安排在多个房间。

图4-28建模的关系是“发言人—在房间中—的会议上—发言”。

[img]http://dl.iteye.com/upload/picture/pic/64051/6fe69af8-4695-39cb-860c-ad569eb78616.jpg[/img]
图4-28 “发言人—在房间中—的会议上—发言”关系

这些实体每一个都是BCNF实体。然而,3个实体间的关系很麻烦,因为键包含了3列,尤其是所有的属性都是从其他实体迁移而来。当然,这样做也可能一点错都没有,但是,任何时候发生了类似于这样的情况时,都需要进行一些调查,确保在我们的数据中没有多值依赖,因为它们最终可能会造成问题。

考虑每个会议都同时开展的情况。此时表中需要会议的时间,而这会使情况更为复杂。在这个时候,我们处理的是所有的会议都用一个会议时间的情况。让我们看一组示例数据(我们将房间、发言人和会议联结了起来,这样就能看到自然键)。

[img]http://dl.iteye.com/upload/picture/pic/64049/77dea5bc-6e1e-3c4c-b4da-680d7d703a57.gif[/img]

第一行没有问题,因为我们这一行是针对会议101,有一个发言人Davidson,在房间River Room中。接下来两行问题开始变得明显起来,因为会议202在一个房间中有两个不同的发言人。这迫使我们在“Room”属性中不必要地重复数据,因为我们现在已经存储了两条会议202是在Stream Room中的信息。如果会议改地方,我们就必须在两处都变更信息,如果我们忘记了这一点,基于目前没显示出来的某个值更新房间信息(比如,通过使用人工键),那我们最终就会得到如下的值。

在这个例子中,我们有重复的“Session”和“Room”属性数据,并且“404”会议有重复的“Session”和“Presenter”数据。当添加或修改数据时,这种数据复制造成的真正的问题就来了。如果我们想要更新Davidson和Hazel在Stream Room中进行发言的“Session”号,则需要修改两行。同样地,如果“Room”分配发生了变化,也有好几行必须修改。

当用这种方式实现实体时,也许不会像这里看到的那样,所有的行的所有列都填满了。接下来,我们会介绍一系列未填满的行,从功能上讲,它们与前面的实体集是等价的。
[img]http://dl.iteye.com/upload/picture/pic/64055/85dbd4ca-9a93-32f8-aa2b-5ebc7be1a951.gif[/img]

在这个例子中,有的房间值为null,有的发言人值也为null。我们消除了重复的数据,但现在我们有一些看起来怪里怪气的数据,到处都是null。不仅如此,我们现在不能用null来清楚地表达“我们还不知道会议的发言人是谁”的情况了。我们确实存储了与前面的例子等价的数据集,但是,这些数据的格式让人很难操作。姑且不论这样做是对是错,你也许可以立刻看出这样的数据操作起来是多么地让人烦恼。

为开发一个该问题的解决方案,让我们首先将Presenter作为主要的实体,如图4-29所示。
[img]http://dl.iteye.com/upload/picture/pic/64047/d96ac7ac-5000-3843-9625-e7d438dc4c9a.jpg[/img]
图4-29 Presenter是主要的实体

然后我们将RoomSessionPresenter实体分解为3个实体,如图4-30所示。
[img]http://dl.iteye.com/upload/picture/pic/64045/a2d02cbb-592d-30e5-ac35-b66b9a27045b.jpg[/img]
图4-30 以发言人为中心重新组织数据

这显然不是个正确的解决方案,因为除非被分配一个发言人,否则我们永远都不可能决定会议在哪个房间中举行。并且,Davidson在River Room和Stream Room中都有会议,但没有什么内容能够链接回去,告诉我们房间中举行的会议是什么。当我们分解关系时,如果我们丢失了数据的意义,这种分解就被称为有损分解。这里的情况就是有损分解,因此它并不是问题的合理解决方案。

接下来我们尝试着以会议举行的房间为中心,如图4-31所示。
[img]http://dl.iteye.com/upload/picture/pic/64043/55139636-9738-3c1c-97aa-fbd52bc63913.jpg[/img]
图4-31 现在Room是主要的实体

将数据分解入实体中,如图4-32所示。
[img]http://dl.iteye.com/upload/picture/pic/64053/9ccadf8e-a04f-3668-8f9a-00ac1d7fb6f0.jpg[/img]
图4-32 以Room为中心重新组织数据

这也是个有损分解,例如,我们不能决定到底是谁在202会议上发言。它在Stream Room房间中,并且Davidson、Hazel和Hawkins都要在Stream Room中发言,但是,他们并不都在202会议上发言。所以,再一次,我们要考虑其他的设计。这一次,我们的设计以要举行的会议为中心,如图4-33所示。

看看图4-34中的数据。

最终,我们找对了该问题的解决方案。从这个数据出发,我们可以准确地决定谁在哪个房间、在哪个会议上发言,并且,添加或删除发言人,或甚至是修改房间都没有问题。拿404会议举例。对于404会议,在如下的结果集中,实体SessionRoom和SessionPresenter包含的数据是:

[img]http://dl.iteye.com/upload/picture/pic/64067/6b4fbf0c-9486-38a0-8aa9-c755d42039b8.jpg[/img]
图4-33 现在Session是主要的实体
[img]http://dl.iteye.com/upload/picture/pic/64063/ef7d1d88-4dd6-33a4-ae3b-669a8a0e7d91.jpg[/img]
图4-34 以会议为中心重新组织数据
[img]http://dl.iteye.com/upload/picture/pic/64065/7d0c1163-1c00-3c70-9be2-4d43265adb01.gif[/img]

要向名单中加入一个名叫Evans的发言人,我们只需简单地加入另一行:
[img]http://dl.iteye.com/upload/picture/pic/64059/4f9d7991-21dd-3b27-bc3d-1b985e93a7d9.gif[/img]

现在,这是个恰当的分解,不会有在先前的实体中所碰到的问题。我们现在有与发言人分开的会议集,在外键值中也不再需要null,因为如果我们想要表示一个房间没有被选中开会,我们就不创建SessionRoom实例。如果我们没有选中一名发言人也同样如此。更重要的是,现在我们为一个会议设定多个房间也不会有什么混乱。

如果需要用其他数据来扩展SessionPresenter这个概念,例如,指定一个备选发言人(或者是第一发言人和第二发言人),现在就有一个明确而合乎逻辑的地方来存储这些信息了。请注意,如果我们曾经试图在原来的实体中存储这些信息,那就会违反BCNF,因为AlternatePresenter属性只会与Session和Presenter有关系,与Room无关。

这个过程的关键是寻找属性之间的关系。对这个例子来说,会议和谁在会议上发言之间有关系,会议和会议的举行地点也有关系。反过来说,发言人与会议在哪里举行之间却没有直接的联系。

2. 潜伏的多值属性

我之所以使用“潜伏的”这个词,是因为粗看起来,在本节讨论的属性并不总会出问题。问题在于,某个属性在很多情况下看起来似乎需要的都仅仅是单值,但是,当仔细琢磨这个问题时,会发现其也有需要多值的时候。为了展示这层意思,让我们看看如图4-35的设计模型。

当我们考虑Contact实体时,问题浮出了水面。我们有3个属性:联系人的名字(假设这个名字满足第一范式)、电话号码和地址。名字没什么问题,因为所有的联系人都有一个用来代表他们自己的名字,问题在于,在这个时代,许多人都有不止一个地址和电话号码。所以我们就有了多值属性,需要进一步规范化来解决这个问题。为了允许多值的地址和电话号码,我们可以像图4-36那样修改设计。
[img]
http://dl.iteye.com/upload/picture/pic/64071/b4ed6d4e-b800-33be-bbaa-fa20d3a3aa4e.jpg[/img]
图4-35 包含了潜伏的多值属性的设计
[img]http://dl.iteye.com/upload/picture/pic/64079/4f0c0474-65ce-3460-a7e5-db5ee41972ef.jpg[/img]
图4-36 模型现在支持多值的地址和电话号码

虽然有多值的电话号码并没有违反第一范式(因为它们都是不同类型的电话号码,而不是同一类型的多个值),然而,它确实给我们带来了更多问题。因为我们只是简单地在属性名上添加了属性的类型(例如,HomeAddressId、FaxPhoneId),所以,如果用户有两个传真号或两个手机号,我们将面临更严重的多值属性问题。不仅如此,当属性的值不存在时,对每个属性我们都需要多个可以为null的值,这种情况当然很不理想,因为从技术上来说,null表示值未知,而不是值不存在。

这样表示关系真是一团糟。例如,如果客户需要对一位联系人增加其配偶的办公室电话号码属性,我们就必须修改模型,而这非常可能需要重写应用程序的代码。

让我们进一步修改设计,将Contact与ContactInformation实体分开,如图4-37所示。

Type属性表明我们存储在实例中的联系人信息的类型,如此一来,我们就可以将某个ContactInformation实例冠以“家庭”(Home)的“类型”(Type),为其附上地址和电话号码。通过这种方式,用户需要多少电话号码和地址,我们就可以加上多少。然而,由于地址和电话号码保存在同一张表中,如果联系人家庭地址和电话号码的数量不同,我们就还是需要在某些值中填入null。
[img]http://dl.iteye.com/upload/picture/pic/64077/6f7970b2-3342-30b3-bbf2-79c6b2142616.jpg[/img]
图4-37 分开Contact和ContactInformation实体

在这个阶段,我们需要决定自己想要如何进行下去。我们可能想要一个电话号码与地址关联起来(例如,将家庭电话号码与家庭地址相关联)。就这个例子而言,我们将把ContactInformation实体分解为ContactAddress和ContactPhone(虽然这并不是该问题唯一可能的解决方案),如图4-38所示。
[img]http://dl.iteye.com/upload/picture/pic/64075/6186dcd0-6abd-3520-8d90-961e13b3a1a4.jpg[/img]
图4-38 用ContactAddress和ContactPhone实体来体现联系人的信息

这个修改消除了残留的多值依赖,因为现在我们可以有许多彼此独立的地址和电话号码,并且,我们可以想定义多少类型就定义多少类型,不需要修改实体的结构。然而,我们还可以再进一步:在逻辑模型中将电话号码和地址建模为不同的实体,再针对Type列加入域实体。这样做之后,我们就可以防止当用户想输入的是“Home”时,却输入 “Some”、“Homer”、“Hume”这样的值。将电话号码和地址建模为不同的实体,还使得我们能够建立用户可配置的约束,这样就可以不修改模型就加入新的类型。我们将在域实体上加入Description属性,它使我们可以说明一个类型的真实用途。Description属性使得我们可以处理如下情况:对一个组织来说,“离开”这种地址类型的意义可能很明白,但对于第一次使用系统的用户来说,其意义却很费解。针对这个类型,我们可以加入说明信息,比如“联系人出差延期,仍然在商务旅行中时的地址”。图4-39显示了我们最终的模型。
[img]http://dl.iteye.com/upload/picture/pic/64073/5f279122-cdfd-36fb-abae-9a3d423d3ce6.jpg[/img]
图4-39 最终模型

注意,我把添加的Address和PhoneNumber属性设置为替代键,为的是避免每次需要在系统中使用它们时都得设置一个重复的地址。通过这种方式,当我们有5个联系人有相同的办公室地址时,我们就只用修改一个值。在最终的实现中,这也许是所期望的东西,也许不是,因为它会增加复杂性;从商业的角度来看,增加这种复杂性也许值得,也许不值得。

事实上,在检查第一范式、第二范式、第三范式或Boyce-Codd范式时,你往往会发现很多这类问题。例如,回头想想讲第一范式时的Payment1、Payment2等,如果这个字段仅仅是Payment,它看起来会那么扎眼吗?也许不会。如果你在搜索实体时非常严格,并且意识到了支付是某种独立于客户的东西,那么你才有可能做对,否则就很容易忽略它。

范式的定义图
[img]http://dl.iteye.com/upload/picture/pic/64081/d15e5593-e06d-33ef-b6a9-a902fdaeb3a8.jpg[/img]
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值