【问题标题】:Should I design a table with a primary key of varchar or int?我应该设计一个主键为 varchar 或 int 的表吗?
【发布时间】:2010-11-21 00:45:49
【问题描述】:

我知道这是主观的,但我想了解人们的意见,并希望在设计 sql server 表结构时可以应用一些最佳实践。

我个人认为在固定(最大)长度的 varchar 上键入表是不行的,因为这意味着还必须在使用它作为外键的任何其他表中传播相同的固定长度。使用int,将避免必须在整个板上应用相同的长度,这必然会导致人为错误,即一个表有varchar (10),另一个有varchar (20)

这听起来像是最初设置的噩梦,而且意味着将来维护表格也很麻烦。例如,假设键控 varchar 列突然变成 12 个字符而不是 10 个字符。您现在必须去更新所有其他表,这可能是多年后的一项艰巨任务。

我错了吗?我在这里错过了什么吗?我想知道其他人对此有何看法,以及坚持使用 int 作为主键是否是避免维护噩梦的最佳方法。

【问题讨论】:

  • 请为您的表格使用数字、离散类型(例如 int/bigint)。或者带有 newsequentialid 的指南,这是另一场辩论......
  • 固定长度的varchar?这甚至有意义吗?您只定义 varchar 的最大长度,而不是大小。 en.wikipedia.org/wiki/Varchar
  • @Andrew - 你明白我的意思,是的,我的意思是固定的最大值。对不起,如果我冒犯了你。

标签: sql sql-server database-design


【解决方案1】:

在选择主键时,您通常还会选择集群键。他们两个经常混淆,但你必须了解其中的区别。

主键是逻辑业务元素。您的应用程序使用主键来标识实体,关于主键的讨论主要是使用natural keyssurrogate key。链接更详细,但基本思想是自然键派生自现有实体属性,如 ssnphone number,而代理键对于业务实体没有任何意义,如 idrowid,它们通常是IDENTITY 类型或某种uuid。我个人的观点是代理键优于自然键,并且选择应该始终是本地应用程序的标识值,任何类型的分布式数据的指南。主键在实体的生命周期内永远不会改变。

聚集键是定义表中行的物理存储的键。大多数情况下,它们与主键(逻辑实体标识符)重叠,但这实际上不是强制执行的,也不是必需的。当两者不同时,意味着表上有一个实现主键的非聚集唯一索引。聚集键值实际上可以在行的生命周期内发生变化,从而导致行在表中物理移动到新位置。如果您必须将主键与集群键分开(有时您会这样做),那么选择一个好的集群键比选择主键要困难得多。有两个主要因素会推动您的集群密钥设计:

  1. 流行的数据访问模式
  2. 存储注意事项

数据访问模式。通过这一点,我了解了查询和更新表格的方式。请记住,聚集键确定表中行的实际顺序。对于某些访问模式,某些布局在查询速度或更新并发性方面有着天壤之别:

  • 当前与存档数据。在许多应用程序中,属于当前月份的数据经常被访问,而过去的数据很少被访问。在这种情况下,表设计按交易日期使用table partitioning,通常使用sliding window 算法。当前月份的分区保存在位于热快速磁盘的文件组上,存档的旧数据被移动到托管在更便宜但速度较慢的存储上的文件组中。显然,在这种情况下,聚集键(日期)不是主键(事务 id)。两者的分离是由规模要求驱动的,因为查询优化器将能够检测到查询只对当前分区感兴趣,甚至不会查看历史分区。

  • FIFO 队列样式处理。在这种情况下,表有两个热点:发生插入的尾部(入队)和发生删除的头部(出队)。集群键必须考虑到这一点,并组织表格以物理分离磁盘上的尾部和头部位置,以允许入队和出队之间的并发性,例如。通过使用入队顺序键。在 pure 队列中,这个聚集键是唯一的键,因为表上没有主键(它包含 消息,而不是 实体)。但大多数时候队列不是纯粹的,它还充当实体的存储,queuetable 之间的界限是模糊的。在这种情况下还有一个主键,它不能是集群键:实体可以重新入队,从而改变入队顺序集群键值,但它们不能改变主键值。未能看到分离是用户表支持的队列如此臭名昭著地难以正确处理并充满死锁的主要原因:因为入队和出队在表中交错发生,而不是定位在队列的尾部和头部。

  • 相关处理。当应用程序设计良好时,它将在其工作线程之间划分相关项的处理。例如,处理器被设计为具有 8 个工作线程(例如匹配服务器上的 8 个 CPU),因此处理器在它们之间划分数据,例如。工人 1 只选取名为 A 到 E 的帐户,工人 2 F 到 J 等。在这种情况下,表实际上应该按帐户名称(或由最左边位置为帐户名称第一个字母的复合键)聚集,以便工作人员在表中本地化他们的查询和更新。这样的表将有 8 个不同的热点,围绕每个工人此刻集中的区域,但重要的是它们不重叠(没有阻塞)。这种设计在高吞吐量 OLTP 设计和 TPCC 基准负载中很普遍,其中这种分区也反映在缓冲池中加载的页面的内存位置(NUMA 位置),但我离题了。

存储注意事项。聚集键 width 在表的存储中具有巨大的影响。一方面,key 在 b-tree 的每个非叶子页面中都占用空间,因此大的 key 将占用更多空间。其次,通常更重要的是,集群键被每个非集群键用作查找键,因此每个非集群键必须存储集群键的完整宽度每一行。这就是使大型聚集键(如 varchar(256) 和 guid 成为聚集索引键的糟糕选择的原因)。
此外,键的选择也会影响聚集索引碎片,有时会严重影响性能。

这两种力量有时可能是对立的,数据访问模式需要某个大的集群键,这会导致存储问题。在这种情况下,当然需要平衡,但没有神奇的公式。您测量并测试以达到最佳状态。

那么我们从这一切中得到什么? 总是从考虑集群键开始,它也是entity_id IDENTITY(1,1) NOT NULL 形式的主键。将两者分开并在适当时相应地组织表格(例如按日期分区)。

【讨论】:

  • “主键在实体的生命周期内永远不会改变。” -- 为什么不呢?
  • @pst:因为其他系统使用主键来识别实体。如果 PK 发生变化,它们的引用会丢失,或者更糟糕的是,引用不同的实体。不能更改内存中对象地址的相同原因:指向它的所有指针都无效。
【解决方案2】:

我绝对建议在每个表中使用 INT NOT NULL IDENTITY(1,1) 字段作为 主键。

使用 IDENTITY 字段,您可以让数据库处理所有细节,以确保它确实是唯一的,并且 INT 数据类型只有 4 个字节,并且是固定的,因此它更容易且更适合用于主要(和聚类)表中的键。

你是对的 - INT 是一个 INT 是一个 INT - 它不会改变任何东西的大小,所以你不必去重新创建和/或更新你的外键关系。

使用 VARCHAR(10) 或 (20) 只会占用太多空间 - 10 或 20 个字节而不是 4 个字节,而且很多人不知道 - 集群键值将在每个索引上重复表上每个非聚集索引上的条目,因此可能会浪费大量空间(不仅在磁盘上 - 这很便宜 - 而且在 SQL Server 的主内存中)。此外,由于它是可变的(可能是 4 个字符,也可能是 20 个字符),SQL Server 很难正确维护良好的索引结构。

马克

【讨论】:

  • 你不使用varchar的原因是什么?和我一样还是其他原因?
  • @Max:如果您想改用 GUID - 将其用作主键,但永远不要将其用作 SQL Server 表上的集群键。非常糟糕 - 糟糕的性能,非常糟糕的索引碎片。
  • @Michael:甚至 newsequentialguid 也只是一个 hack - 它仍然会导致大量索引碎片 - 而且使用起来很麻烦 - SELECT * FROM TABLE WHERE OID = 'ab3-123-123ba-09213' -我就是想不起来那样的东西......
  • 另外,将 GUID 用作 PK 的问题在于,在大多数情况下它们正在被使用,这是因为开发人员希望能够在客户端上设置 GUID——而不是让数据库处理工作--> 我认识的人几乎没有人真正使用过 newsequentialid() :-(
  • @Thomas:是的,与 INT IDENTITY 合并很麻烦 - 但可以管理。但实际上:你必须处理多少次?将其与发送到该表的 EVERY 查询的查询性能差(使用 GUID 作为聚类键)进行比较.....
【解决方案3】:

我同意一般来说,INT(或身份)字段类型是大多数“正常”数据库设计中的最佳选择:

  • 它不需要“算法”来生成 id/key/value
  • 你有快速(er)连接,优化器可以在范围内更努力地工作,等等
  • 您遵循的是事实上的标准

也就是说,您还需要了解您的数据。如果您要处理带符号的 32 位 int,则需要考虑无符号的。如果您要解决这个问题,也许您想要的是 64 位整数。或者,您可能需要一个 UUID/hash 来更轻松地在数据库实例/分片之间进行同步。

不幸的是,这取决于 YMMV,但我肯定会使用 int/identity,除非你有充分的理由这样做。

【讨论】:

    【解决方案4】:

    正如您所说,一致性是关键。我个人使用无符号整数。除非您正在处理大量可笑的数据,否则您不会用完它们,并且您始终可以知道任何关键列都需要是该类型,并且您永远不必为各个列寻找正确的值。

    【讨论】:

      【解决方案5】:

      基于无数次地进行这个练习,然后用结果支持系统,对于 INT 总是更好的笼统陈述有一些警告。一般来说,除非有理由,否则我会同意。但是,在战壕中,这里有一些优点和缺点。

      INT

      • 除非有充分的理由不这样做,否则使用。

      GUID

      • 唯一性 - 一个例子是程序的远程部分之间只有一种通信方式,而需要启动的一方不是数据库一方。在这种情况下,在不选择 INT 的情况下,在远程设置 Guid 是安全的。
      • 再次唯一性 - 一个更牵强的场景是多个客户共存于不同数据库中的系统,并且使用一套程序在类似用户等客户之间进行迁移。如果该用户注册另一个程序,他们的用户记录可以在那里使用而不会发生冲突。另一种情况是,如果客户从彼此那里获取实体。如果两者都在同一个系统上,他们通常会期望迁移更容易。本质上,客户之间的任何频繁迁移。
      • 难以使用 - 即使是经验丰富的程序员也无法记住 guid。在进行故障排除时,必须为查询复制和粘贴标识符通常令人沮丧,尤其是在使用远程访问工具完成支持的情况下。经常引用 SELECT * FROM Xxx WHERE ID = 7 比 SELECT * FROM Xxx WHERE ID = 'DF63F4BD-7DC1-4DEB-959B-4D19012A6306' 容易得多

      • 索引 - 为 guid 字段使用聚集索引需要不断重新排列数据页,并且索引效率不如 INT 甚至短字符串。它会影响性能 - 不要这样做。

      CHAR

      • 可读性 - 虽然传统观点认为没有人应该进入数据库,但系统的现实是人们可以访问 - 希望是您组织中的人员。当这些人不熟悉连接语法时,如果没有许多其他查询,带有 int 或 guid 的规范化表就不清楚。带有一些字符串键的同一个规范化表对于故障排除更有用。我倾向于将其用于在安装时提供记录的表类型,因此它们不会变化。当键为“已关闭”或“待处理”时,主表上的 StatusID 之类的东西比数字更可用于支持。在这些领域使用传统密钥可以将一个容易解决的问题变成需要开发人员帮助的问题。即使是让有问题的人员访问数据库造成的,这样的瓶颈也很糟糕。
      • 约束 - 即使您使用字符串,也要让它们保持固定长度,这样可以加快索引速度并添加约束或外键以防止垃圾进入。有时使用此字符串可以让您删除查找表并将选择保留为代码中的简单枚举 - 限制进入此字段的数据仍然很重要。

      【讨论】:

      • 使用 GUID 作为聚集索引是一种致命的罪过,并且会扼杀你所有的性能——即使你每晚都重建索引!只是不要这样做 - 句号。
      • @marc_s 同意。我编辑了答案以更有力地说明这一点。谢谢。
      【解决方案6】:

      对于best performance,99.999% 的时间主键应该是单个整数字段。

      除非您要求主键在数据库中的多个表或多个数据库中是唯一的。我假设您在询问 MS SQL-Server,因为这就是您的问题被标记的方式。在这种情况下,请考虑改用 GUID 字段。虽然比 varchar 好,但 GUID 字段的性能不如整数。

      【讨论】:

        【解决方案7】:

        使用 INT。您的积分都是有效的;我会优先考虑:

        1. 易于使用 SQL 自动增量功能 - 为什么要重新发明轮子?
        2. 可管理性 - 您不想更改关键字段。
        3. 性能
        4. 磁盘空间

        1 和 2 需要开发人员的时间/精力/努力。 3 和 4 你可以扔硬件。

        【讨论】:

          【解决方案8】:

          如果乔·塞尔科在场,他会说一些严厉的话... ;-)

          我想指出,INT 作为硬性规定并不总是合适的。假设您有一个车辆表,其中包含所有类型的汽车卡车等。现在假设您有一个 VehicleType 表。如果您想获得所有卡车,您可以这样做(使用 INT 身份种子):

          SELECT V.Make, V.Model
          FROM Vehicle as V
          INNER JOIN VehicleType as VT
          ON V.VehicleTypeID = VT.VehicleTypeID
          WHERE VT.VehicleTypeName = 'Truck'
          

          现在,在 VehicleType 上使用 Varchar PK:

          SELECT Make, Model
          FROM Vehicle 
          WHERE VehicleTypeName = 'Truck'
          

          代码更简洁一些,您可以避免加入。也许联接并不是世界末日,但如果您的工具箱中只有一个工具,那么您将失去一些提高性能和更清晰架构的机会。

          只是一个想法。 :-)

          【讨论】:

          • 你右边的第二个查询看起来更干净。这真的很重要吗?难道你不能使用 ID's' 并且仍然通过使用视图来保持这个干净吗?您确定第二种方法会带来性能提升吗,正如其他帖子所说的相反。干杯。
          • 是的,虽然您将因此交换一些数据存储和缓存内存,但它的 CPU 性能会更高。性能提升来自于将整数相互连接。在 int 上进行比较比在 char 上进行比较的成本更低。但是在我提供的情况下,您根本不需要加入。在这种情况下,加入比不加入更昂贵。我的观点是你不应该对数据库设计采取绝对主义的方法。
          • 连接方法对性能的影响听起来非常小,我想除非您的表绝对庞大,否则您不会注意到任何影响,对吧?如果是这种情况,那么带有 int ID 的好的设计肯定更好吗?
          • 好吧,与其用性能统计数据创建一个示例(制作模型数据库需要一点时间 ;-)),不如看看这篇详细介绍人工/自然的文章/代理键:intelligententerprise.com/… 我会写另一条关于性能影响的评论。
          【解决方案9】:

          虽然一般建议使用INT,但这确实取决于您的情况。

          如果您关心可维护性,那么其他类型也同样可行。例如,您可以非常有效地使用 Guid 作为主键。不这样做是有原因的,但一致性不是其中之一。

          但是是的,除非您有充分的理由不这样做,否则 int 是最简单易用的,并且最不可能给您带来任何问题。

          【讨论】:

          • 我同意这在很大程度上取决于您的情况。在某些情况下,我认为 varchar 比 int 更有用,因为它增加了可读性。与往常一样,存在性能权衡,但仅与编码有关 - 您必须决定性能或阅读能力
          • @Dan - 我不赞成这种“可读性”论点。这不就是视图的用途吗?
          【解决方案10】:

          对于 PostgreSQL,我通常使用“Serial”或“BigSerial”“数据类型”来生成主键。这些值是自动递增的,我总是发现整数很容易使用。它们本质上等同于设置为“auto_increment”的 MySQL 整数字段。

          【讨论】:

            【解决方案11】:

            人们应该认真考虑 32 位范围是否足以满足您的工作需求。 Twitter 的状态 ID 是 32 位 INT,用完时会出现问题。

            在这种情况下是否使用 BIGINT 或 UUID/GUID 值得商榷,我不是数据库专家,但 UUID 可以存储在固定长度的 VARCHAR 中,而无需担心您需要更改字段大小。

            【讨论】:

            • 好点 - 如果 20 亿行可能很紧,请改用 BIGINT :-) 这应该会让铁杆推特用户开心一段时间 :-)
            【解决方案12】:

            我们必须记住,表的主键不应该有“业务逻辑”,它应该只是它所属记录的标识。遵循这个简单的规则,一个 int 尤其是一个 identity int 是一个非常好的解决方案。通过询问 varchar 我猜你的意思是使用例如“全名”作为“人”表的键。但是,如果我们想将名称从“George Something”更改为“George A.Something”怎么办?该字段的大小是多少?如果我们改变大小,我们也必须改变所有外表的大小。所以我们应该避免按键上的逻辑。有时我们可以使用社交 ID(整数值)作为键,但我也避免这样做。现在,如果一个项目有扩大规模的前景,您也应该考虑使用 Guids(唯一标识符 SQL 类型)。

            【讨论】:

              【解决方案13】:

              请记住,这是一个相当古老的问题,我仍然想为未来的读者提供使用带有代理键的 varchar 的理由:

              1. 具有多台复制机器的环境
              2. 需要在实际插入之前知道要插入行的 ID 的场景(即,客户端分配此 ID,而不是数据库)

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2011-12-22
                • 2016-06-30
                • 1970-01-01
                • 2010-10-24
                • 1970-01-01
                • 2011-02-09
                • 2010-10-18
                相关资源
                最近更新 更多