在 SQL Server 2012 中索引 PK GUID

njk*_*oes 14 sql-server clustered-index uniqueidentifier index-tuning

我的开发人员已将他们的应用程序设置为使用 GUID 作为几乎所有表的 PK,默认情况下 SQL Server 已在这些 PK 上设置聚集索引。

该系统相对年轻,我们最大的表只有一百万多行,但我们正在查看我们的索引并希望能够快速扩展,因为在不久的将来可能需要它。

所以,我的第一个倾向是将聚集索引移动到 created 字段,它是 DateTime 的 bigint 表示。但是,我可以使 CX 独一无二的唯一方法是在此 CX 中包含 GUID 列,但按创建顺序排列。

这是否会使集群键太宽,是否会提高写入性能?读取也很重要,但此时写入可能是一个更大的问题。

Mik*_*Fal 21

GUID 的主要问题,尤其是非顺序的,是:

  • 键的大小(16 字节对 INT 的 4 字节):这意味着您将在键中存储 4 倍的数据量以及任何索引的额外空间(如果这是您的聚集索引)。
  • 索引碎片:由于键值的完全随机性质,几乎不可能对非顺序 GUID 列进行碎片整理。

那么这对您的情况意味着什么?这归结为您的设计。如果您的系统只是关于写入并且您不关心数据检索,那么 Thomas K 概述的方法是准确的。但是,您必须记住,通过采用这种策略,您会在读取和存储数据时产生许多潜在问题。正如Jon Seigel指出的那样,您还将占用更多空间,并且基本上会出现内存膨胀。

围绕 GUID 的主要问题是它们的必要性。开发人员喜欢它们是因为它们确保了全局唯一性,但这种唯一性是必要的,这种情况很少见。但请考虑,如果您的最大值数小于 2,147,483,647(4 字节有符号整数的最大值),那么您可能没有为您的密钥使用适当的数据类型。即使使用 BIGINT(8 个字节),您的最大值也是 9,223,372,036,854,775,807。如果您需要某个唯一键的自动递增值,这对于任何非全局数据库(以及许多全局数据库)通常就足够了。

最后,就使用堆与聚集索引而言,如果您纯粹是在写入数据,堆将是最有效的,因为您可以最大限度地减少插入的开销。但是,SQL Server 中的堆对于数据检索效率极低。我的经验是,如果您有机会声明一个聚集索引,那么它总是可取的。我已经看到向表(超过 40 亿条记录)添加聚集索引将整体选择性能提高了 6 倍。

附加信息:


Tho*_*ser 13

GUID 作为 OLTP 系统中的键和集群没有任何问题(除非表上有很多索引会因集群大小的增加而受到影响)。事实上,它们比 IDENTITY 列更具可扩展性。

人们普遍认为 GUID 是 SQL Server 中的一个大问题——在很大程度上,这完全是错误的。事实上,GUID 在超过 8 个核心的机器上可以显着提高可扩展性:

我很抱歉,但您的开发人员是对的。在担心 GUID 之前先担心其他事情。

哦,最后:为什么你首先想要一个集群索引?如果您关心的是具有大量小索引的 OLTP 系统,那么使用堆可能会更好。

现在让我们考虑碎片化(GUID 将引入的)对您的读取有何影响。碎片化存在三个主要问题:

  1. 页拆分成本磁盘 I/O
  2. 半满页的内存效率不如满页
  3. 它会导致页面乱序存储,从而降低顺序 I/O 的可能性

由于您在问题中关注的是可扩展性,我们可以将其定义为“添加更多硬件使系统运行得更快”,因此这些问题最少。依次处理每一个

广告 1) 如果您想要规模,那么您可以买得起 I/O。即使是便宜的三星/英特尔 512GB SSD(几美元/GB)也能让您获得超过 100K 的 IOPS。您不会很快在 2 插槽系统上消耗它。如果你遇到这种情况,再买一个就可以了

广告 2)如果您在表格中进行删除,无论如何您都会有半个完整的页面。即使你不这样做,内存也很便宜,除了最大的 OLTP 系统之外的所有系统 - 热数据应该适合那里。当您在寻找规模时,希望将更多数据打包到页面中是次优的。

广告 3)由频繁的页面拆分、高度碎片化的数据构建的表以与顺序填充的表完全相同的速度执行随机 I/O

关于连接,您可能会在类似 OLTP 的工作负载中看到两种主要的连接类型:散列和循环。让我们依次看看:

散列连接:散列连接假定扫描小表并通常寻找较大的表。小表很可能在内存中,所以在这里 I/O 不是您关心的问题。我们已经触及了这样一个事实,即碎片索引中的查找成本与非碎片索引中的成本相同

循环连接:将查找外部表。相同的费用

您可能还会进行很多糟糕的表扫描 - 但是 GUID 再次不是您关心的问题,正确的索引才是。

现在,您可能正在进行一些合法的范围扫描(尤其是在加入外键时),在这种情况下,与非碎片数据相比,碎片数据的“打包”程度较低。但是让我们考虑一下您可能会在索引良好的 3NF 数据中看到哪些连接:

  1. 来自表的连接,该表具有对其引用的表的主键的外键引用

  2. 另一种方式

广告 1)在这种情况下,您要对主键进行一次搜索 - 将 n 连接到 1。碎片与否,相同的成本(一次搜索)

广告 2) 在这种情况下,您正在加入同一个键,但可能检索多于一行(范围查找)。这种情况下的连接是 1 到 n。但是,您寻找的外部表,您正在寻找 SAME 键,它很可能在碎片索引中与非碎片索引位于同一页上。

考虑一下这些外键。即使您“完美”地按顺序放置了我们的主键 - 指向该键的任何内容仍然是非顺序的。

当然,您可能正在某家银行的某个 SAN 中的虚拟机上运行,​​这些银行资金便宜且流程高。那么所有这些建议都将丢失。但是,如果那是您的世界,那么可扩展性可能不是您所寻找的——您正在寻找性能和高速/成本——两者是不同的东西。


小智 5

托马斯:你的一些观点完全有道理,我都同意。如果您使用的是 SSD,那么您优化的平衡确实会发生变化。随机与顺序与旋转磁盘的讨论不同。

我特别同意采用纯 DB 视图是非常错误的。让您的应用程序缓慢,不可扩展,以提高数据库的性能可以很误导。

IDENTITY(或序列,或DB 中生成的任何东西)的大问题是它非常慢,因为它需要往返 DB 来创建密钥,这会自动成为 DB 的瓶颈,它强制应用程序必须进行数据库调用以开始使用密钥。创建 GUID 通过使用应用程序创建密钥来解决这个问题,它保证全局唯一(根据定义),因此应用程序层可以使用它在发生数据库往返之前传递记录。

但我倾向于使用 GUID 的替代方法 我个人对这里的数据类型的偏好是由应用程序生成的全局唯一 BIGINT。怎么做呢?在最简单的示例中,您向应用程序添加了一个很小的、非常轻量级的函数来散列 GUID。假设您的散列函数快速且相对较快(例如,请参阅Google 的 CityHash示例 - 确保您正确完成所有编译步骤,或者FNV的 FNV1a 变体用于简单代码)这将使您受益于应用程序生成的唯一标识符和CPU 可以更好地使用的 64 位密钥值。

还有其他生成 BIGINT 的方法,在这两种算法中都有可能发生哈希冲突 - 阅读并做出有意识的决定。

  • 我建议您将答案编辑为 OP 问题的答案,而不是(现在)作为对 Thomas 答案的答案。您仍然可以突出 Thomas (, MikeFal's) 和您的建议之间的差异。 (2认同)
  • 请回答您对问题的回答。如果你不这样做,我们会为你删除它。 (2认同)
  • 感谢马克的评论。当您编辑您的答案时(我认为这提供了一些非常好的上下文),我会改变一件事:如果您小心插入,IDENTITY 不需要到服务器的额外往返。您始终可以在调用 INSERT 的批处理中返回 SCOPE_IDENTITY()。 (2认同)