我继承了一个基于组合键比使用唯一对象 ID 字段更理想的想法构建的数据库,并且在构建数据库时,不应将单个唯一 ID 用作主键。因为我正在为这个数据库构建一个 Rails 前端,所以我遇到了让它符合 Rails 约定的困难(尽管可以使用自定义视图和一些额外的 gem 来处理复合键)。
这种特定模式设计背后的原因来自编写它的人,这与数据库如何以非有效方式处理 ID 字段以及在构建索引时树排序存在缺陷有关。这种解释缺乏任何深度,我仍在尝试围绕这个概念展开思考(我熟悉使用复合键,但不是 100% 的时间)。
任何人都可以提供意见或增加这个话题的深度吗?
大多数常用引擎(MS SQL Server、Oracle、DB2、MySQL 等)使用代理键系统不会遇到明显问题。有些人甚至可能会因使用代理而获得性能提升,但性能问题是高度特定于平台的。
一般而言,自然键(以及扩展的复合键)与代理键的辩论历史悠久,看不到可能的“正确答案”。
自然键(单数或复合)的参数通常包括以下内容:
1) 它们已经在数据模型中可用。大多数被建模的实体已经包含一个或多个属性或属性组合,这些属性或属性组合满足了创建关系的关键需求。向每个表添加一个附加属性会带来不必要的冗余。
2) 它们消除了对某些联接的需要。例如,如果您有带有客户代码的客户和带有发票编号的发票(两者都是“自然”键),并且您想要检索所有特定客户代码的发票编号,您只需使用 "SELECT InvoiceNumber FROM Invoice WHERE CustomerCode = 'XYZ123'"
。在经典的代理键方法中,SQL 看起来像这样:"SELECT Invoice.InvoiceNumber FROM Invoice INNER JOIN Customer ON Invoice.CustomerID = Customer.CustomerID WHERE Customer.CustomerCode = 'XYZ123'"
。
3) 它们有助于为数据建模提供更普遍适用的方法。使用自然键,相同的设计可以在不同的 SQL 引擎之间基本不变地使用。许多代理密钥方法使用特定的 SQL 引擎技术来生成密钥,因此需要更加专业化的数据模型才能在不同的平台上实现。
代理键的参数往往围绕特定于 SQL 引擎的问题:
1) 当业务需求/规则发生变化时,它们可以更轻松地更改属性。这是因为它们允许将数据属性隔离到单个表中。这主要是 SQL 引擎无法有效实现标准 SQL 结构(如 DOMAIN)的问题。当属性由 DOMAIN 语句定义时,可以使用 ALTER DOMAIN 语句在模式范围内执行对属性的更改。不同的 SQL 引擎在更改域时具有不同的性能特征,并且一些 SQL 引擎根本不实现域,因此数据建模者通过添加代理键来弥补这些情况,以提高对属性进行更改的能力。
2)它们比自然键更容易实现并发。在自然键情况下,如果两个用户同时处理相同的信息集,例如客户行,并且其中一个用户修改了自然键值,那么第二个用户的更新将失败,因为他们是客户代码数据库中不再存在更新。在代理键情况下,更新将成功处理,因为不可变的 ID 值用于标识数据库中的行,而不是可变的客户代码。然而,允许第二次更新并不总是可取的——如果客户代码发生变化,则可能不允许第二个用户继续他们的更改,因为行的实际“身份”已经改变——第二个用户可能更新错误的行。代理键和自然键本身都不能解决这个问题。全面的并发解决方案必须在关键的实现之外解决。
3)它们比自然键表现更好。性能最直接地受到 SQL 引擎的影响。由于 SQL 引擎的数据存储和检索机制,使用不同 SQL 引擎在相同硬件上实现的相同数据库模式通常具有显着不同的性能特征。一些 SQL 引擎非常接近平面文件系统,当相同的属性(例如客户代码)出现在数据库模式中的多个位置时,数据实际上是冗余存储的。当需要对数据或模式进行更改时,SQL 引擎的这种冗余存储可能会导致性能问题。其他 SQL 引擎在数据模型和存储/检索系统之间提供了更好的分离,允许更快地更改数据和模式。
4) 代理键在某些数据访问库和 GUI 框架中发挥更好的作用。由于大多数代理键设计的同质性(例如:所有关系键都是整数),数据访问库、ORM 和 GUI 框架可以处理信息,而不需要对数据的特殊知识。自然键,由于其异构性质(不同的数据类型、大小等),不能很好地与自动化或半自动化工具包和库一起使用。对于专门的场景,例如嵌入式 SQL 数据库,在设计数据库时考虑到特定的工具包可能是可以接受的。在其他情况下,数据库是企业信息资源,由多个平台、应用程序、报表系统和设备同时访问,因此在设计时侧重于任何特定的库或框架时,它们的功能不会那么好。此外,当引入下一个伟大的工具包时,旨在与特定工具包一起使用的数据库成为一种负担。
我倾向于支持自然键(显然),但我对此并不狂热。由于我工作的环境,我帮助设计的任何给定数据库都可能被各种应用程序使用,我在大多数数据建模中使用自然键,并且很少引入代理。但是,我不会不遗余力地尝试重新实现使用代理的现有数据库。代理键系统工作得很好——无需更改已经运行良好的东西。
有一些优秀的资源讨论了每种方法的优点:
http://www.google.com/search?q=natural+key+surrogate+key
http://www.agiledata.org/essays/keys.html
http://www.informationweek.com/news/software/bi/201806814
我已经开发数据库应用程序 15 年了,但我还没有遇到过非代理键比代理键更好的选择。
我并不是说这种情况不存在,我只是说当您考虑实际开发访问数据库的应用程序的实际问题时,通常代理键的好处开始压倒非的理论纯度- 代理键。
主键应该是恒定的且无意义的;非代理键通常最终无法满足一项或两项要求
如果密钥不是恒定的,那么您未来的更新问题可能会变得非常复杂
如果密钥不是无意义的,那么它更有可能改变,即不是恒定的;看上面
举一个简单的常见示例:库存物品表。将商品编号(sku 编号、条形码、零件代码或其他任何内容)作为主键可能很诱人,但一年后,所有商品编号都发生了变化,您将面临一个非常混乱的更新——整个——数据库问题...
编辑:还有一个比哲学更实际的问题。在许多情况下,您会以某种方式找到特定的行,然后更新它或再次找到它(或两者兼而有之)。使用复合键,可以在 WHERE 子句中跟踪更多数据,并为重新查找或更新(或删除)提供更多约束。与此同时,关键部分之一也可能发生了变化!使用代理键,始终只保留一个值(代理 ID),根据定义,它不能更改,这大大简化了情况。
听起来创建数据库的人在自然键与代理键争论的自然键方面。
我从未听说过 ID 字段上的 btree 有任何问题,但我也没有深入研究过它......
我落在代理键方面:使用代理键时重复较少,因为您只在其他表中重复单个值。由于人类很少手动加入表格,我们不在乎它是否是数字。此外,由于在索引中只有一个固定大小的列要查找,因此可以安全地假设代理项也具有更快的主键查找时间。
使用“唯一(对象)ID”字段可以简化连接,但您应该致力于让其他(可能是复合)键仍然唯一——不要放松非空约束并保持唯一约束。
如果 DBMS 不能有效地处理唯一整数,它就有很大的问题。但是,同时使用“唯一(对象)ID”和另一个键确实比仅使用另一个键使用更多空间(用于索引),并且每个插入操作都有两个索引要更新。所以它不是免费赠品——但只要您也保留原始密钥,那么您就可以了。如果你去掉了另一个键,你就破坏了你的系统设计;所有的地狱最终都会崩溃(你可能会也可能不会发现地狱崩溃了)。
我基本上是代理键团队的成员,即使我欣赏和理解 JeremyDWill 在这里提出的论点,我仍在寻找“自然”键比代理更好的情况......
其他处理这个问题的帖子通常是指关系数据库理论和数据库性能。另一个有趣的论点,在这种情况下总是被遗忘,与表规范化和代码生产力有关:
每次我创建一个表,我是否会浪费时间
识别它的主键和它的物理特性(类型、大小)每次我想在我的代码中引用它时记住这些特性?向团队中的其他开发者解释我的 PK 选择?
我对所有这些问题的回答是否定的:
在处理人员列表时,我没有时间浪费尝试识别“最佳主键”。我不想记住我的“计算机”表的主键是一个 64 个字符长的字符串(Windows 是否接受这么多字符作为计算机名称?)。我不想向其他开发人员解释我的选择,其中一位最终会说“是的,伙计,但考虑到您必须管理不同域的计算机?这个 64 个字符的字符串是否允许您存储域名 +计算机名称?”。
因此,在过去的五年中,我一直在遵循一个非常基本的规则:每个表(我们称之为“myTable
”)都有其第一个名为“id_MyTable
”的字段,它属于 uniqueIdentifier 类型。即使此表支持“多对多”关系,例如“ComputerUser
”表,其中“id_Computer
”和“id_User
”的组合形成了一个非常可接受的主键,我更喜欢创建这个“id_ComputerUser
”字段作为唯一标识符,只是为了遵守规则。
主要优点是您不必再关心代码中主键和/或外键的使用。获得表名后,您就知道 PK 名称和类型。一旦您知道在数据模型中实现了哪些链接,您就会知道表中可用外键的名称。
我不确定我的规则是不是最好的。但它是一个非常有效的!
开发新体系结构的一种实用方法是对包含数千个多列高度唯一记录的表使用代理键和用于简短描述表的组合键。我通常发现大学规定使用代理键,而现实世界的程序员更喜欢复合键。您确实需要将正确类型的主键应用于表 - 而不仅仅是一种方式。
使用任何自动 ORM 作为持久层,使用自然键是一场噩梦。此外,多列上的外键往往会相互重叠,这在以 OO 方式导航和更新关系时会产生进一步的问题。
您仍然可以在唯一约束中转换自然键并添加自动生成的 id;但是,这并不能消除外键的问题,但必须手动更改;希望多列和重叠约束将是所有关系的一小部分,因此您可以专注于重构最重要的地方。
自然 pk 有他们的动机和使用场景,并不是一件坏事(tm),他们只是倾向于与 ORM 相处得不好。
我的感觉是,与任何其他概念一样,应该在合理的情况下使用自然键和表规范化,而不是作为盲目的设计约束
我将在这里简短而甜蜜:这些天复合主键并不好。如果可以,添加代理任意键并通过唯一约束维护当前键方案。 ORM 很高兴,你很高兴,原始程序员不太高兴,但除非他是你的老板,否则他可以处理它。
复合键可能很好——它们可能会影响性能——但它们不是唯一的答案,就像唯一(代理)键不是唯一的答案一样。
让我担心的是选择复合键的推理含糊不清。对任何技术的含糊不清往往表明缺乏理解——也许遵循别人的指导方针,在书或文章中......
单个唯一 ID 没有任何问题 - 事实上,如果您将应用程序连接到数据库服务器,并且您可以选择使用哪个数据库,那么一切都会很好,并且您几乎可以使用您的密钥和还不算太惨。
已经并且将会有很多关于此的文章,因为没有单一的答案。有些方法和方法需要以熟练的方式谨慎应用。
我在数据库自动提供 ID 方面遇到了很多问题——我尽可能避免使用它们,但偶尔仍会使用它们。
...数据库如何以非有效方式处理 ID 字段以及在构建索引时,树排序存在缺陷...
这几乎可以肯定是无稽之谈,但可能与从不同会话以高速率将递增数字分配给 PK 时的索引块争用问题有关。如果是这样,那么 REVERSE KEY 索引就可以提供帮助,尽管由于块分割算法的变化而以更大的索引大小为代价。 http://download.oracle.com/docs/cd/B19306_01/server.102/b14220/schema.htm#sthref998
进行合成,特别是如果它有助于使用您的工具集进行更快速的开发。
我不是一个有经验的人,但我仍然赞成使用主键作为 id 这里是使用示例的解释..
外部数据的格式可能会随着时间而改变。例如,您可能认为一本书的 ISBN 会成为图书表中的一个很好的主键。毕竟,ISBN 是独一无二的。但随着这本书的编写,美国的出版业正在为重大变革做准备,因为所有 ISBN 都添加了额外的数字。如果我们使用 ISBN 作为图书表中的主键,我们必须更新每一行以反映这种变化。但接下来我们会有另一个问题。数据库中还有其他表通过主键引用 books 表中的行。除非我们首先检查并更新所有这些引用,否则我们无法更改 books 表中的键。这将涉及删除外键约束、更新表、更新 books 表,最后重新建立约束。总而言之,这是一种痛苦。如果我们使用我们自己的内部值作为主键,问题就会消失。任何第三方都不能出现并任意告诉我们改变我们的模式——我们控制着我们自己的键空间。如果确实需要更改诸如 ISBN 之类的内容,它可以在不影响数据库中任何现有关系的情况下进行更改。实际上,我们已经将行的编织与这些行中数据的外部表示分离。
虽然解释很书,但我认为它以更简单的方式解释了事情。
@JeremyDWill
感谢您为辩论提供了一些急需的平衡。特别感谢您提供有关 DOMAIN
的信息。
为了一致性起见,我实际上在系统范围内使用代理键,但需要权衡取舍。我诅咒使用代理键的最常见原因是当我有一个包含规范值的简短列表的查找表时 - 我会使用更少的空间,如果我刚刚创建值,我的所有查询都会更短/更容易/更快PK 而不是必须加入表。
您可以两者兼得——因为任何大公司的数据库都可能被多个应用程序使用,包括运行一次性查询和数据导入的人类 DBA,因此纯粹为了 ORM 系统的利益而设计它并不总是实用或可取的。
这些天我倾向于做的是为每个表添加一个“RowID”属性——这个字段是一个 GUID,因此对每一行都是唯一的。这不是主键 - 这是一个自然键(如果可能)。但是,在此数据库之上工作的任何 ORM 层都可以使用 RowID 来识别它们的派生对象。
因此,您可能有:
CREATE TABLE dbo.Invoice ( CustomerId varchar(10), CustomerOrderNo varchar(10), InvoiceAmount money not null, Comments nvarchar(4000), RowId uniqueidentifier not null default(newid()), primary key(CustomerId, CustomerOrderNo) )
因此,您的 DBA 很高兴,您的 ORM 架构师很高兴,并且您的数据库完整性得以保留!
我只是想在这里添加一些我在讨论与关系数据库自动生成的整数标识字段时从未见过的内容(因为我经常看到它们),也就是说,它的基本类型可能会在某些时候溢出。
现在我并不是说这会自动使复合 id 成为可行的方法,但事实上,即使可以将更多数据逻辑添加到表中(仍然是唯一的),单个自动生成的整数身份可以防止这种情况发生。
是的,我意识到在大多数情况下这不太可能,并且使用 64 位整数可以为您提供很大的空间,实际上,如果发生这样的溢出,数据库可能应该采用不同的设计。
但这并不能阻止某人这样做......使用单个自动生成的 32 位整数作为其身份的表,预计将在全球范围内存储特定快餐公司的所有交易,将失败,因为一旦它尝试插入它的第 2,147,483,648 笔交易(这是一个完全可行的场景)。
需要注意的是,人们倾向于掩饰或完全忽略。如果要定期插入任何表,则应考虑随时间累积数据的频率和数量,以及是否应使用基于整数的标识符。