什么时候主键才有意义?

Man*_*ngo 7 database-design primary-key candidate-key

请原谅这里任何定义的松散,但我试图探索一个简单的概念。

一个主键唯一标识一行。表中可能还有其他列具有唯一值,因此它们也可以唯一标识一行(候选键),但主键是为任务指定的。

使主键更有用的属性包括:

  • 保证是唯一的;随着表的增长,其他一些唯一的列值可能会重复
  • 不太可能改变;虽然外键约束可以包括级联,但最好不要要求这个
  • 不会被回收;由于某种原因被删除的行不应释放旧的 PK 值以供重用

由于这些原因,我通常建议主键没有内在价值,因此永远没有理由更改或回收值。也就是说,它应该是没有意义的

我见过包含某种代码的主键,例如基于名称的客户端代码。明显的问题是 (a) 如果客户端名称更改,那么 PK 也应该更改,以及 (b) 与具有相似名称的客户端发生冲突的风险太大。

半个例外是使用自动递增的数字,它具有序列号的次要含义。但是,它仍然很稳定。

问题是,在什么情况下,如果有的话,最好使用具有其他实际含义的主键?也就是说,PK应该是任意的,并且你通常可以通过序列号获得的建议有什么问题吗?

Sol*_*zky 7

在什么情况下,如果有的话,最好使用具有其他实际含义的主键?(强调)

鉴于这个问题的重点是“可取”而不是“可接受”,并且接受这仍然是一个高度主观的话题,我会说我想不出最好的情况是系统有一个真正自然的出于各种原因的关键(其中大部分已经在保罗在对该问题的评论中链接的其他答案中说过):

  1. 被认为是唯一的并不总是唯一的(例如,美国的社会安全号码 / SSN)
  2. 有时事情会发生变化,无论是价值还是独特性(我们无法控制外部世界)
  3. 即使某些东西在价值和唯一性上应该是“稳定的”(例如 SKU,也许),能否保证传入的价值是正确的?人类在进行数据输入时经常会打错字。导出过程中也存在错误,可能导致系统导入的文件中的值不正确。其他系统中也存在将数据输入到您的系统中的错误,这些错误可能导致数据本身不完全正确,即使它们的导出过程正常工作。

我强调“真正”,因为有两种情况我更喜欢没有新的代理键:

桥接表

(或任何您喜欢称呼的表格,仅用于或主要用于表示多对多关系)

Thing                          ThingXTag                Tag
------                         ---------                ---
ThingID INT AutoMagic PK --->  ThingID INT PK, FK      
Stuff   SomeType               TagID   INT PK, FK  <--- TagID   INT AutoMagic PK
                                                        TagName VARCHAR
Run Code Online (Sandbox Code Playgroud)

在对桥接表(逻辑模型中不存在但物理模型中需要的表)进行建模时,PK 应该存在通过该表关联的表的现有主键列。这允许在不需要单独的唯一索引/约束的情况下强制执行值的正确唯一性和非 NULL 性。在这种关系需要外键的罕见情况下,它将:

  • 有意义的是,每个键列实际上都将指向原始源表,而无需另一个连接,并且
  • 防止某人或某事更新形成两个主表之间关系的键列,而不更新通过外键链接的值。
WackyTable                           ThingXTag
----------                           ---------
WackyTableID INT AutoMagic PK
ThingID      INT FK            --->  ThingID     INT PK, FK (to Thing.ThingID)
TagID        INT FK            --->  TagID       INT PK, FK (to Tag.TagID)
AttributeX   VARCHAR                 InsertDate  DATETIME
InsertDate   DATETIME
Run Code Online (Sandbox Code Playgroud)

我曾在一个系统上工作,其中这些桥接表有自己的自动递增代理键 PK,并且桥接表的单列代理键通过 FK 在其他表中引用:

WackyTable                           ThingXTag
----------                           ---------
WackyTableID INT AutoMagic PK        
ThingXTagID  INT FK            --->  ThingXTagID INT AutoMagic PK
AttributeX   VARCHAR                 ThingID     INT FK (to Thing.ThingID)
InsertDate   DATETIME                TagID       INT FK (to Tag.TagID)
                                     InsertDate  DATETIME
Run Code Online (Sandbox Code Playgroud)

这是一个可怕的、令人困惑的混乱,我们浪费了太多时间进行调试等。

同级表

这些表是真正的单个实体,因此具有 1 对 1 的关系。出于性能原因,它们仅被拆分为两个(或更多,根据需要)表。我已经对具有 100 万(或更多)行的表进行了此操作,这些行要么非常宽,要么宽度适中,并且有些列要么不经常使用,要么是超过 50 个字节的字符串。像那样的东西。这将实体的核心属性保留在一个更窄的表中,该表适合每个数据页上的更多行。

在这些情况下,“兄弟”表与初始表处于完全相同的级别,并且应该具有相同的PK. 给它一个自动递增的代理键是没有用的,因为每一行都有相当于初始表的自然键。

Product                            ProductProperty
-------                            ---------------
ProductID  INT AutoMagic PK  --->  ProductID        INT PK, FK (to Product.ProductID)
Name       VARCHAR                 ShortDescription VARCHAR
SKU        VARCHAR                 SomethingElse    SomeType
Quantity   INT                     UpdateDate       DATETIME
CreateDate DATETIME
UpdateDate DATETIME
Run Code Online (Sandbox Code Playgroud)

需要明确的是,我说的是物理模型,而不是概念模型。我认为这个问题的重点是物理模型,因为它是在概念上不存在的问题的上下文中构建的:代理键、处理主键值使用的问题等。考虑到这一点,我不是暗示不应存储自然密钥并将其用于识别。相反,自然键是很好的“备用键”,应该在其上放置唯一的约束/索引。然而,概念模型的理想主义并不总是直接转化为物理模型。数据的完整性(即,数据模型的稳定性和可靠性)是顶,如果不是top,物理模型的优先级。因此,必须进行实际考虑,例如使用代理键,以确保实现这一目标而不是妥协。意思是,如果您有 SSN 或 SKU 等,那么绝对将它们存储在具有唯一约束的列中,并让系统对该值进行查找因为无论如何都不应该在外部使用自动生成的数字。用户不需要知道记录的自动生成的 ID 号:他们应该传入一个他们知道的值(例如,电子邮件地址作为 UserID/CustomerID 的查找,航班确认代码与航班日期等)并且系统应该将其转换为它从那时起使用的自动生成的值。

是的,在使用自然键作为备用键时,本答案开头提到的问题仍然是潜在问题。但是,不同之处在于问题被隔离到(通常)只有一张表。如果有人犯了一个错误并在“航班定位器”上创建了一个唯一索引,那么他们可能需要一段时间才能获得违规行为。但是一旦他们这样做了,就很容易删除该唯一索引并重新创建它以包括航班日期。或者,如果您在系统上更改您的电子邮件地址(通常用作登录名)并由于其他人几年前(合法地)使用它而收到错误,那么这很可能由支持人员处理,而不会对您造成任何影响/风险现有的相关记录。在这两种情况下,数据模型的其余部分都不会进行必要的更改。

同样,这是一种务实的方法:

  • 最小化迁移一个或多个外键引用的主键时可能发生的潜在数据丢失(并且最小化项目范围也会减少维护窗口:-)。虽然并非所有 PK(或唯一约束/索引)都具有 FK,但使用代理肯定会减少具有此类依赖性的列数
  • 使系统尽可能持久、有弹性和高效。物理模型是概念模型的“实践”到“理论”。鉴于概念模型不关心实现,我们已经做了很多调整和考虑。然而,为了性能(例如反规范化、查找表、如上所述的“同级”表等)、创建“桥接”表(如上所述)等,我们做出选择以利用供应商特定的特性。

我不知道有多少系统使用 SSN(美国的社会安全号码)作为 PK,但对于使用 SSN(美国的社会安全号码)作为 PK 的系统,其中一些(也许很多)可能避免了它们不像它们应该那样独特的问题。但是,多年来,这些系统都无法避免发生变化,因为需要更安全地处理它们。将 SSN 视为备用密钥的系统需要很少的开发时间来切换到加密这些值,并且系统几乎不需要(或不需要)停机时间来在数据层进行更改。鉴于我们都有可能永远无法完成的项目积压,企业往往更愿意让这些烦人但不可避免的更改花费 5 小时而不是 20 到 40 个小时(不要忘记更改需要测试,


明确地说,在某些情况下,拥有自然键是“可以接受的”,尽管我认为我不会说“可取”。

  1. 州/地区和国家/地区代码:如果您使用由国际标准化组织 (ISO)维护的代码(例如“US”= 美国,“FL”= 佛罗里达等),那么这些代码可能足够可靠,可以使用,我有使用它们是因为它们很短(即不利于性能)和人类可读。这些代码也经常以各种其他方式使用,甚至在计算机系统之外,人们对它们普遍熟悉,甚至那些最初对某些人来说意义不大的代码(例如“DE”=对于那些不知道德语中的“德国”==“德国”的人来说,德国可能并不直观)。
  2. 内部查找值代码:您无法控制外部来源,但(希望)您可以控制自己的系统。如果您的系统具有内部使用的部门代码、状态代码等,那么为它们提供代码(2 - 4 个字节)应该没问题。在 4 个字节时,它将使用与 an 相同的空间量,INT如果使用二进制排序规则(以_BIN2, 甚至结尾_BIN,但_BIN2首选),那么它的比较速度应该一样快。对此类代码具有相对有意义的值可以使支持/调试更容易。但是,您仍然会遇到这样的情况:随着时间的推移,部门名称等可能会发生变化,并且代码可能不再有意义。