选择主键:自然键还是代理键?
本文概述关系数据库中为表指定主键的策略。主要关注于何时使用自然键或者代理键的问题。有些人会告诉你应该总是使用自然键,而另外一些人会告诉你应该总是使用代理键。这些人总是被证明是错误的,通常他们仅仅是与你分享了他们“数据信仰”的偏见。事实上自然键与代理键具有各自的优缺点,没有在所有情况下都完美的策略。也就是说,你必须清楚你要做的事情才能做好它。本文讨论以下内容:
- 常用的术语
- 代理键实现策略
- 有效选键的技巧
- 做出了“错误”选择时如何应对
1、常用的术语
让我们从描述一些关于键的常用术语开始,然后再看一个示例。这些术语包括:
键。键是唯一标识一个实体的一个或者多个数据属性。在物理数据库中,键可以由表的一个或者多个列组成,它们的值唯一标识关系表中的一行。
复合键。由两个或者多个属性组成的键。
自然键。由现实世界中已经存在的属性组成的键。例如,美国公民被分配了一个唯一(不保证一定正确,但实际上非常接近唯一)的社会保险号(SSN)。如果隐私法允许的话,SSN可能被用作Person实体的自然键(假设组织的人员范围仅限于美国)。
代理键。不具有业务含义的键。
候选键。在逻辑数据模型中的实体类型可能具有0或多个候选键,简称为唯一标识(注解:某些人不主张在逻辑数据模型中标识候选键,因此没有固定标准)。例如,假设我们只涉及美国公民,那么SSN是Person实体类型的一个候选键,同时姓名与电话号码的组合(假设组合是唯一的)是第二个可能的候选键。这两个键都被称作候选键是因为它们是物理数据模型中主键、次键或者非键的候选目标。
主键。实体类型的首选键。
备用键。也就是次键,是表中行的另一个唯一标识。
外键。在一个实体类型中表示另一个实体类型的主键或者次键的一个或多个属性。
图1显示了使用UML符号描述的实际地址的物理数据模型。在图1中,表Customer使用CustomerNumber作为主键而SocialSecurityNumber作为备用键。这就意味着访问顾客信息的首先方法是通过一个人的顾客编号,虽然软件使用社会保险号也能够获得相同的信息。表CustomerHasAddress拥有一个复合键,由CustomerNumber与AddressID组成。外键是一个实体类型中,代表另一个实体类型的主键或者次键的一个或多个属性。外键用于维护数据行之间的关联。例如,表CustomerHasAddress与表Customer中行之间的关联通过表CustomerHasAddress的CustomerNumber列进行维护。有趣的是列CustomerNumber既是表CustomerHasAddress主键的一部分,又是表Customer的外键。同样,列AddressID既是表CustomerHasAddress主键的一部分,又是表Address的外键,维护这两个表中行的关联。
2、比较自然键与代理键策略
为表指定键的策略有两种:
自然键。自然键是已经存在的一个或多个属性,它在业务概念中是唯一的。对于表Customer来说,存在两个候选键,CustomerNumber与SocialSecurityNumber。
代理键。引入一个不具有业务含义的列作为键,称作代理键。例如图1中表Address的列AddressID。地址不具有一个“简单”的自然键,因为需要使用Address表的所有列组成一个键(取决于你的问题域,可能仅仅需要组合Street和ZipCode列),所以此时引入一个代理键是一个更好的选择。
自然键的优点
是它们已经存在,不需要在数据模式中引入一个新的“非自然”列。
自然键的缺点
自然键的缺点是由于具有业务含义,它们与业务直接耦合:你可能在业务需求变更时重新指定键。例如,当你的用户决定将CustomerNumber列从数字型改为字母数字型,除了更新表Customer的模式(这个是不可避免的)外,你还需要修改每一个使用CustomerNumber作为外键的表。
代理键的优点
- 首先,它们不与业务耦合,因此更容易维护(假设你选择了一个好的实现策略)。
例如,如果表Customer改为使用代理键,修改只需要在表Customer内部进行(此时CustomerNumber只是表的一个非键列)。当然,如果你需要针对代理键策略做相似的变更,可能是由于用完了所有的值而需要增加几个位数,将会面临同样的问题。
- 其次,一个大多数表,最好是全部表,通用的键策略能够减少需要编写的源码数量,减少系统的总体拥有成本(TCO)。
代理键的缺点
它们通常不是“人可读的”,导致终端用户难以使用。这意味着你可能仍然需要实现代理键用于查找、编辑等等。
根本问题在于键是关系模式中重要的耦合源,因此它们很难更改。这意味着你通常想要避免具有业务含义的键,因为业务含义存在变化。话虽如此,我倾向于使用自然键查找/引用表,尤其当我认为键值在最近不会改变时,如下文所述。从根本上讲,是否应该优先使用自然键没有明确的答案,不管这个宗教之争的另外一方狂热者如何声称,最好的策略是只要有意义就可以使用任何一个策略。
3、代理键实现策略
实现代理键有几个常用的选择:
- 使用数据库赋值。
大多数主要的数据库供应商--例如Oracle、Sybase以及Informix--实现了被称为递增键的代理键策略。基本理念是在数据库服务器中维护一个计数器,将当前值写入一个隐藏的系统表来维护一致性,并用于赋值一个新建的数据行。每创建一行,计数器递增并将值作为该行的键值。不同供应商的实现策略不同,有时候值在所有表之间都是唯一的,有时候只在单个表内部是唯一的,但是基本概念相同。
- MAX() + 1。
一个常用的策略是使用整数列,第一条记录从1开始,然后新行的值设置为该列的最大值加1,最大值用SQL函数MAX获得。虽然这个方法简单,但是对于大表存在性能问题,而且它只能确保表内部的唯一键值。
- 全局唯一标识符(UUIDs)。
GUIDs是128位值,来自以太网卡ID或等价的软件表示以及系统当前时间的哈希值。该算法是由开放软件基金会定义的。
- 全球唯一标识(GUIDs)。
GUIDs是微软扩展UUIDs后的标准,遵从相同的策略,如果存在以太网卡使用网卡ID,如果不存在,使用软件ID与当前时间计算一个哈希值,确保在机器内部唯一。
- 高低位策略。
它的基本思想是键值,通常称为持久化对象标识符(POID)或者简称对象标识符(OID),分为两个逻辑部分:从指定来源获取的唯一HIGH值和应用自身分配的N为LOW值。每获取一个HIGH值,LOW值设置为0。例如,应用请求一个HIGH值并被赋予1701。假设LOW值的位数N为4,那么赋予对象的POID将会由17010000、17010001、17010002等等直到17019999组成。此时,再获取一个新的HIGH值,LOW值设置为0,再次重复。如果另一个应用在之后立即请求了一个HIGH值,它将获得1702,而它创建的对象被赋予OIDs将会是17020000、17020001等等。正如你所看到的,只要HIGH值唯一,所有的POID值将会唯一。在www.theserverside.com上可以找到一个HIGH-LOW发生器的实现。
根本问题在于键是关系模式中重要的耦合源,因此它们很难重构。这意味着你通常想要避免具有业务含义的键,因为业务含义存在变化。然而,同时你需要记住某些数据通常是通过唯一标识进行访问,例如通过顾客编号访问顾客信息,通过社会保险号访问美国雇员信息。在这种情况下你可能想要使用自然键而不是UUID或者POID这样的代理键。
4、有效选键技巧
如何有效地选择键?参考以下提示:
- 避免“智能”键。
“智能”键是由一个或多个具有业务含义的部分组成的键。例如,美国邮政编码的前两位表示它所在的州。智能键的第一个问题是它具有业务含义。其次是它们的使用通常随着时间变得很复杂。例如,一些大的州拥有多个代码,加利福尼亚的邮编以90和91开头,导致基于州编码的查询更加复杂。第三个问题是它们通常增加了策略需要进行扩展的可能性。考虑长度为9位数字的邮编(后4位数字由建筑所有者自行决定,建筑由邮编唯一标识),在用完2位州代码前用完9位数字的可能性更小。
- 考虑为简单的“查找”表指定自然键。
“查找”表是用于关联代码与详细信息的表。例如,你可能拥有一个列出了颜色代码对应颜色名称的查找表。例如,代码127代表“郁金香黄色”。简单的查找表通常包含一个代码列和一个描述/名称列,而复杂的查找表包含一个代码列和几个信息列。
- 自然键并非总是适用于“查找”表。
另一个例子是一个查找表包含北美洲的州、省或者地区。例如美国的加利福尼亚州以及加拿大的安大略省。该表的主要目的是为这些地理位置提供一个正式的列表,它不会随时间变化(最近一次变化是90年代后期,加拿大的西北地区分割为努勒维特和西北地区)。该表的一个有效自然键可以是州代码,一个值唯一的两字符代码--例如加利福尼亚的CA以及安大略的ON。不幸的是这种方法并不适合,因为加拿大政府决定为西北地区两个州使用相同的代码NW。
- 应用必须仍然支持“自然键搜索”。
如果选择采用代理键,必须不能忘了应用需要支持基于地域列(仍然唯一标识数据行)的搜索。例如,Customer表可能拥有一个Customer_POID的代理键,以及一个Customer_Number列和Social_Security_Number列。你很可能需要支持基于顾客编号和社会保险号的搜索。搜索在关系数据库对象检索最佳实践中详细讨论。
- 不要自然化代理键。
一旦你向终端用户显示了代理键的值,或者更坏的是允许他们使用该值(例如搜索该值),实际上你已经给它们赋予了业务含义。这实际上是自然化了代理键从而失去了代理键的优点。
5、做出了“错误”选择时如何应对
首先,不用为此担心:不论你多么擅长数据库设计都可能会犯错。好消息是正如我在数据库重构过程中所说,虽然可能需要许多工作,还是可以使用代理键替换自然键的(反之亦然)。要使用代理键替换自然键,你需要应用引入代理键重构,如图2所示。要使用自然键替换代理键,你需要应用使用自然键替换代理键重构,如图3所示。
原文地址:http://www.agiledata.org/essays/keys.html