我应该使用varchar还是int作为主键设计表格?

22

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

我个人认为,在一个固定(最大)长度的 varchar 上建立表键是不可取的,因为这意味着必须在使用此作为外键的任何其他表中传播相同的固定长度。使用 int 可以避免在整个表中应用相同的长度,这肯定会导致人为错误,例如一个表有 varchar(10),而另一个表有 varchar(20)。

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

我错了吗?我错过了什么吗?我想知道别人对此的看法,如果使用 int 作为主键是否是避免维护噩梦的最佳方法。


请务必为您的表使用数字离散类型(例如int / bigint)。或者使用带有newsequentialid的guid,这是另一个讨论话题... - Michael Haren
固定长度的varchar?这有什么意义吗?你只需要定义一个varchar的最大长度,而不是大小。 http://zh.wikipedia.org/wiki/Varchar - Andrew Lewis
2
@Andrew - 你明白我的意思了吗,是的,我指的是固定的最大值。如果冒犯到你了,对不起。 - HAdes
13个回答

50

在选择主键时通常也会选择聚集键。这两者经常被混淆,但您必须理解它们的区别。

主键是逻辑上的 业务 元素。应用程序使用主键来识别实体,并且关于主键的讨论主要是是否使用自然键代理键。这些链接提供了更详细的信息,但基本思想是自然键源自现有实体属性,如 ssn电话号码,而代理键对于业务实体没有任何意义,比如 idrowid,它们通常是 IDENTITY 类型或某种 uuid。我的个人观点是,代理键优于自然键,选择本地应用程序始终使用标识值,选择分布式数据使用 guid。主键在实体的生命周期内永远不会改变。

聚集键是定义表中行的物理存储的键。大多数情况下,它们与主键(逻辑实体标识符)重叠,但实际上并不强制执行或要求这两者相同。当二者不同时,这意味着表上有一个非聚集唯一索引实现了主键。聚集键的值实际上可以在行的生命周期内更改,导致将该行在表中移动到新位置。如果必须将主键与聚集键分开(有时确实需要这样做),则选择好的聚集键比选择主键要困难得多。主要驱动聚集键设计的因素有两个:

  1. 普遍的数据访问模式
  2. 存储考虑因素

数据访问模式。我的理解是指查询和更新表格的方式。请记住,聚集键确定了表中行的实际顺序。对于特定的访问模式,某些布局在查询速度或更新并发性方面起到了非常重要的作用:

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

  • 使用FIFO队列风格进行处理。在这种情况下,表格有两个热点:插入发生的尾部(enqueue)和删除发生的头部(dequeue)。集群键必须考虑到这一点,并组织表格以在物理上分离磁盘上的尾部和头部位置,以允许enqueue和dequeue之间的并发性,例如使用enqueue顺序键。在纯粹的队列中,这个集群键是唯一的键,因为表格上没有主键(它包含消息,而不是实体)。但是大多数时候,队列不是纯粹的,它也作为实体的存储,队列和表格之间的界限变得模糊。在这种情况下,还有一个主键,它不能是集群键:实体可以重新入队,从而更改enqueue顺序集群键值,但它们不能更改主键值。没有看到这种分离是用户表格支持的队列非常难以正确处理且容易出现死锁的主要原因:因为enqueue和dequeue会交错地发生在表格中,而不是局部地发生在队列的尾部和头部。

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

  • 存储考虑事项。集群键的宽度在表的存储方面具有巨大的影响。首先,这个键占用了B树的每个非叶页的空间,因此一个大的键将占用更多的空间。其次,而且通常更为重要的是,聚簇键被每个非聚簇键用作查找键,因此每个非聚簇键都必须为每一行存储聚簇键的完整宽度。这就是为什么像varchar(256)和guids这样的大聚簇键不适合用作聚簇索引键的原因。
    此外,选择键对聚簇索引碎片化也有影响,有时会严重影响性能。

    这两个力量有时会相互对立,数据访问模式需要某个大的聚簇键,但这会导致存储问题。在这种情况下,当然需要平衡考虑,但没有魔法公式。您需要进行测量和测试才能找到最佳点。

    那么我们应该怎么做呢? 始终首先考虑聚簇键,它也是形式为entity_id IDENTITY(1,1) NOT NULL的主键。在适当的情况下,将其与表分开并进行组织(例如按日期进行分区)。


    "实体的生命周期中,主键永远不会改变。" -- 为什么不会改变? - user166390
    8
    由于其他系统使用主键来识别实体,因此如果主键更改,则它们的引用将丢失或更糟糕的是引用另一个实体。这也是为什么不能更改内存中对象的地址的原因:对它的所有指针都会变得无效。 - Remus Rusanu

    18

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

    有了IDENTITY字段,您可以让数据库处理确保它真正是唯一的和所有详细信息,而INT数据类型仅为4字节,固定大小,因此更适合用作表中的主(和聚集)键。

    而且,您是正确的——INT就是INT——它不会更改其大小或任何内容,因此您永远不必重建和/或更新外键关系。

    使用VARCHAR(10)或(20)占用太多空间——10或20字节而不是4字节,而且许多人不知道的是——群集键值将在表上的每个单个非聚集索引条目上重复出现,因此潜在地浪费了很多空间(不仅在磁盘上——那很便宜——但也在SQL Server的主存储器中)。 另外,由于它是可变的(可能是4个字符,也可能是20个字符),所以SQL服务器更难以正确维护良好的索引结构。

    Marc


    @HAdes 人为错误总是有可能的。我的理由只是因为没有人这样做,它有异味,而且可能不太高效,但我并不确定。 - Max Schmeling
    4
    如果你想使用 GUID,那么请将其作为主键 PRIMARY KEY,但永远不要将其用作 SQL Server 表的聚集键(clustering key)。这样会导致很差的性能和非常严重的索引碎片化。 - marc_s
    1
    @Michael:即使是newsequentialguid也只是一个hack - 它仍然会导致大量的索引碎片化 - 而且使用起来很麻烦 - SELECT * FROM TABLE WHERE OID = 'ab3-123-123ba-09213' - 我就是记不住这种东西...... - marc_s
    1
    此外,将GUID用作PK的问题在于,在大多数情况下使用它们的原因是开发人员希望能够在客户端上设置GUID,而不是让数据库处理工作--> 我认识的人中几乎没有人真正使用newsequentialid() :-( - marc_s
    1
    @Thomas:是的,使用INT IDENTITY进行合并确实很麻烦,但还是可以处理的。但是,真的需要多少次才能处理这个问题呢?与每次发送到该表的查询都使用GUID作为聚集键导致的糟糕查询性能相比呢...... - marc_s
    显示剩余6条评论

    4

    一般来说,在大多数“正常”的数据库设计中,INT(或identity)字段类型是最好的选择:

    • 它不需要“算法”来生成id/key/value
    • 您可以更快地进行连接,并且优化器可以在幕后更加努力地处理范围等问题
    • 您正在遵循一种事实上的标准

    话虽如此,您还需要了解您的数据。如果您将通过有符号32位int,您需要考虑无符号。如果您将通过这个,也许64位ints是您想要的。或者也许您需要一个UUID /哈希来使数据库实例/分片之间的同步更容易。

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


    2

    就像你所说,保持一致性是关键。我个人使用无符号整数。除非处理大量数据,否则不会用尽它们,而且您始终可以知道任何关键列需要该类型,并且您永远不必寻找单个列的正确值。


    2

    通过无数次的实践和支持系统结果,有一些关于INT总是更好这个断言的注意事项。通常情况下,除非有理由不这样做,我会同意这个说法。然而,在实际操作中,以下是一些优缺点。

    INT

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

    GUID

    • 唯一性 - 一个例子是在程序的远程部分和需要发起请求的数据库不在同一侧的情况下。在这种情况下,在远程端设置Guid是安全的,而选择INT则不安全。
    • 再次唯一性 - 一个更遥远的场景是存在多个客户在不同的数据库中共存,并且之间存在迁移,例如类似用户使用一套程序。如果该用户注册了另一个程序,则可以在那里使用其用户记录而不会冲突。另一个场景是客户从彼此处获取实体。如果两者在同一系统上,则通常希望迁移更容易。基本上,任何频繁的客户之间的迁移。
    • 难以使用 - 即使是经验丰富的程序员也无法记住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
    1
    @marc_s同意。我编辑了答案,更加强调了这一点。谢谢。 - Jim Blake

    1

    如果Joe Celko在这里,他可能会有一些严厉的话... ;-)

    我想指出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'
    

    代码更加简洁,避免了使用join。也许join并不是世界末日,但如果你的工具箱中只有一种工具,那么你就会错过一些提高性能和清晰架构的机会。

    仅供参考。 :-)


    你是对的,第二个查询看起来更简洁。但这真的很重要吗?你不能使用ID并通过视图保持代码整洁吗?你确定第二种方法有性能提升吗?因为其他帖子说相反的事情。谢谢。 - HAdes
    2
    是的,从 CPU 的角度来看,它将更具性能,尽管作为结果,您将交换一些数据存储和缓存内存。性能提升来自于将整数彼此连接。与 char 相比,在 int 上进行比较的成本较低。但在我提供的情况下,您根本不必进行连接。在这种情况下,连接比不连接更昂贵。我的观点是,您不应该采取数据库设计的绝对主义方法。 - Anon246
    连接方法的性能影响似乎极小,除非您的表格非常庞大,否则您不会注意到任何影响,对吧?如果是这种情况,那么使用 int ID 进行良好设计肯定更好,不是吗? - HAdes
    不妨不用创建一个带有性能统计数据的示例(制作模拟数据库需要一些时间;-)),为什么不看一下这篇详细介绍人工/自然/代理键的文章:http://www.intelligententerprise.com/showArticle.jhtml;jsessionid=UPLU504AQT1OBQE1GHOSKHWATMY32JVN?articleID=201806814&pgno=2我会在另一个评论中写出性能影响。 - Anon246

    1

    为了获得最佳性能, 99.999%的时间主键应该是一个单一的整数字段。

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


    1

    使用INT。您提出的所有观点都是有效的,我会按以下优先顺序进行:

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

    1和2需要开发人员的时间/精力/努力。 3和4可以通过硬件解决。


    0

    尽管这是一个相当老的问题,但我仍然想为未来的读者提出使用带有代理键的varchar的论点:

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

    0

    虽然通常建议使用INT,但这取决于您的情况。

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

    但是,是的,除非您有充分的理由不这样做,否则int是最简单的选择,并且最不可能引起任何问题。


    我同意这在很大程度上取决于你的情况。在某些情况下,我认为varchar比int更有用,因为它增加了可读性。像编码一样,总是存在性能权衡,你必须决定是优先性能还是可读性。 - Dan
    @Dan - 我不赞同这个“可读性”的论点。那不是视图的目的吗? - HAdes

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接