逻辑模型通过定义必要的数据表、表间关系及其元组数目而澄清了应用程序的实现领域。在逻辑设计阶段要定义每一表列的属性,比如是否接受空值、默认值、规则以及各类检查和约束等。然后就可以把设计应用到特定的关系数据库管理系统 (RDBMS)——就像在地面的圆坑中打上方木桩。
在前面的文章 里我曾经提出过一个旅行社的例子,这个旅行社要面对4类客户:
接受佣金的其他旅行社
享受打折优惠、拥有一些旅行者的企业客户
特权组织或者俱乐部,它们接受佣金其客户享受打折服务。
零售服务的客户
我们考虑针对各个类型的客户分别创建客户表,但是这种方法会产生若干问题。更好的解决方案是建立唯一客户表,通过类型字段区分每一客户,这样就需要第2个数据表来定义客户的类型。这样,我们的逻辑模型就如图A所示。当然,真正的应用程序会复杂得多,不过我们在这里宁肯让有关的问题和程序来得简单一些。
图A
逻辑数据模型的简单视图
在结合客户表之后,我们的设计就包含了定义客户类型的一个新表。
解决规范化问题
给出客户表并没有完全终止规范化过程,我们还要考虑以下问题:
企业旅行者的姓名字段还没有成为最小的可能单元,从而破坏了1NF(First Normal Form)原则。这些旅行者可以是单独的游客而非公司。不过解决这个问题也很简单,我们可以创建两个字段:FirstName和LastName,然后做出些有关定义。
新的客户表也有个性质比较严重的问题:接受零售服务的客户很可能是需要FirstName和LastName字段的个人。为了简化示例,我们不妨规定以下一条商务规则:所有的客户都是公司,所以在目前阶段还不需要解决这个问题。出于简单的目的,我们只引入了这一条商务规则,把客户限定为企业而非个人,这样就不再需要进一步地设计了。
地址信息破坏了2NF(Second Normal Form)规则。地址字段和其他完整说明客户的所有字段而非地址字段都存在依附性。所以,某些开发人员可能会取消新表中的地址字段。不过那样做的话,如果数据表按照BCNF规则规范化(Boyce-Codd Normal Form)则可能引入额外的数据表。在这种起来下,你可以引入地址、城市和州等数据表来遵守以上规则。此外,客户还可能具有多个地址,比如说,一个地址专门用来通邮;一个地址专门用来寄送票据和日程表信息等等。
客户表中的打折和佣金字段也有问题,它们倒没有违反什么规范化规则,但是这类字段很多往往会是空白。空字段虽然也没什么坏处,但最好还是尽量避免出现这样的情况。事实上,太多的空白往往是出现规范化错误的信号。所以,就我们的例子来说,两个字段都完全说明了主键而且不会与其他非键字段形成明显的依附关系。不过,由于可能出现空白字段的缘故,我们还是把这些字段移到一个新表,通过客户标识每条记录,然后给佣金或者折扣标记百分比。
客户只会有一个电话号码吗?如果不是这样,客户表内当前的电话号码字段就破坏了1NF规则:禁止出现多值字段。那就是说,一个字段内不允许输入多个电话号码。所以电话号码字段也必须移到一个新表中来,这样就需要一个电话号码表。解决的办法是从客户表中删除电话号码字段然后创建一个只有电话号码的新表。
规范化还是不规范化
如果我们的目标是老实遵守BCNF原则,那么地址表的规范化(Normalizing)就是必须考虑的问题,我们需要按照客户类型来标识每一地址:商务、服务交付等等。第1步是从客户表中删除地址字段,然后根据图B所显示的初始列表创建新的地址表。但我们还必须接着解决州和城市字段之间的依附性问题。
图B
新地址表按照以上初始列表创建
这样又引入了两个问题。首先,图B中的条目没有实现完全的规范化。该表破坏了3NF规则,因为非键字段之间存在依附性:城市和州字段互相依附。为了对该表实现规范化:
从地址列表中删除邮政编码(ZIP代码)、城市、州和国家字段。
创建邮政编码表,把刚才删除的字段移到该新表。
在地址表中作为外键插入邮政编码主键。
某些开发人员可能会争辩说让数据表遵守BCNF可能太过分了。有些人则认为数据库设计的规范化总是一种简单又低廉的设计方式。事实上,某些开发者还可能更进一步创建城市、州甚至国家表来避免出现重复性的组。但是,就现在来看,我们主要关注的是解决依附性问题。资源和需求之间的平衡还谈不上对与错。
第2个问题刚才我们已经提到过了,但这个问题不打算在本文中得到解决。一个用在某特定小地区的应用程序也许并不需要采用一个大型的数据表来保存城市、州、ZIP代码和国家等信息。另一方面,如果将来有可能扩大业务,你就应当考虑相关设计来谋取成功。你要确定应用程序是否需要大型的、耗费资源和降低性能的数据表来保存城市、州、ZIP代码和国家值等客户可能永远都用不着的信息。在做了以上修改之后我们的设计蓝图中又冒出了一些新表,如图C所示。
图C
采用若干新表对地址信息实现完全的规范化
从逻辑模型迁移到物理模型
数据表实现规范化之后,下面我们就应该着手选择目标数据库,把逻辑模型转变为物理模型。且不说在理论上SQL的移植性到底如何,实际上不同的系统提供了不同的功能、扩展,当然也少不了bug。这些附加值说真的纯粹是市场因素使然:每家开发商都在设法扩展SQL语言,希望诱导开发人员依赖于其独特和强大的语法。这样一来,共享数据思想实际上变得模糊起来,代价也越来越高昂,显然应用程序太依赖于系统扩展了(这也正是开发商们所希望的)。
Microsoft SQL Server 2000提供了UDF(用户定义函数)这种语言扩展,而它并没有受到其他竞争产品的支持。微软公司在特立独行方面可不是唯一的家伙。Oracle的NESTED TABLE语法以及MySQL之流都提出了相当数量的个性扩展,比如后者的ENUM、SET列类型等。
所有的开发商都在玩这种游戏,说起来也不能算什么坏主意,毕竟消费者多了不少选择。不过这也正是数据建模的主要原因(尤其是在开发大型应用程序的情况下)。与特定产品无关是最令人高兴的——这简直就是数据库的天堂。就数据建模的工具选择来说,你尽可听听音乐、想想自己的设计,选择一个目标然后按下按钮产生脚本,由它以适合目标的语言来创建数据库。否则,时间花了不少不说,在实现阶段解决问题都够你受的。
逻辑表不总能转变为物理表
此时,你必须根据目标数据库所提供的功能重新检查你的设计。偶然的情况下,某些逻辑模型中的设计可能无法变成实际的表。比如说,逻辑模型要求我们创建若干个表来标识以下类型:客户、电话号码、费用和地址。如果简单的检查约束就能提供好得多的解决方案,那么某些乃至以上所有的附加表都可能是不必要的。幸而我们的目标系统SQL Server就支持检查约束。所谓检查约束就是限制表列允许可能值的规则。比如,某些小型整数列可以接受256个值,但是检查约束则可以把允许值限制为最多20个值。
凡事有好就有坏,检查约束对规则也不例外。系统应该有能力给约束表添加新项目,对许多应用程序而言这可能是个很烦人的活。请参考比较各个方案、表或者检查约束的优点和缺点。
UDF和存储计算
SQL Server开发者如此热衷于UDF有一个重要原因:你可以在SELECT语句中用到它们。我们可以通过示例程序说明。现在假设我们根据一年内的每月销售额付佣金。这样设计一个简单的UDF来接受旅行社主键(也可能是日期)并返回包含去年该旅行社全年每月销售额的12列数据表。这样做有两个优点:
不需要额外的存储。
没有保存总数和实际总和之间出现偏差的危险(有这种可能,因为某个bug使得更新总和表出错)。
另一方面,UDF当然比保存销售总额操作要慢一些。如果性能是主要关注的问题而选择了表存储则会出现以下问题:
必须用到一个13列的表,它由客户主键和每月销售额组成。
或者创建一个三列的数据表(客户主键、月份和月销售额),每个月的数据作为一行记录。
后者意味着必须访问12行而不是一行数据,所以在处理速度上会更慢一些,如果数据记录成千上万则运行速度就值得注意了。这样做也有优点,没有破坏任何数据。不需要覆盖各行数据;只需要增加一个新行作为月销售额传递数据即可。然后假如你确定修改规则,比如考虑最近两年的数据而非最近一年,则有关模型都不需要修改,只需要访问客户的24行而非12行数据即可。
当然,计算得出的销售总表也有自己的问题。虽然它并没有明显地破坏3NF原则,不过这个方案要求新的检查和平衡。只要有人更新了特定定单,系统就必须重新计算该客户的月销售额。由于这个缘故,即便UDF的速度确实较慢,在不考虑性能可能显著降低的情况下至少还是个更合理的选择,因为它只在必要的时候才运行。那就意味着结果值会反映最新的数据。
所以建模遇到了一个两难的情况:性能和额外的工作量之间必须做出选择。如果选择UDF则性能会降低(可能)。如果选择销售总表则会在实际的开发过程中耗费更多的时间来保证计算最后佣金所需数据的准确性。这里没有对错之分,业务和用户(有时)决定了最终的解决方案。不过这也说明建模过程是何等重要。完全地规范化数据表并不一定总是正确的答案。图D就比较了SQL Server同MySQL和Oracle的处理比较。