为什么在我的测试案例中,顺序的GUID密钥比顺序的INT密钥执行得更快?


39

询问后这个问题比较顺序和非顺序的GUID,我试图表与顺序初始化的GUID主键比较上1 INSERT性能)newsequentialid(),和2)的表与INT主键与顺序初始化identity(1,1)。我希望后者是最快的,因为整数的宽度较小,并且生成顺序整数比顺序GUID似乎也更简单。但是令我惊讶的是,带有整数键的表上的INSERT显着慢于顺序GUID表。

这显示了测试运行的平均时间使用量(毫秒):

NEWSEQUENTIALID()  1977
IDENTITY()         2223

谁能解释一下?

使用了以下实验:

SET NOCOUNT ON

CREATE TABLE TestGuid2 (Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

CREATE TABLE TestInt (Id Int NOT NULL identity(1,1) PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

DECLARE @BatchCounter INT = 1
DECLARE @Numrows INT = 100000


WHILE (@BatchCounter <= 20)
BEGIN 
BEGIN TRAN

DECLARE @LocalCounter INT = 0

    WHILE (@LocalCounter <= @NumRows)
    BEGIN
    INSERT TestGuid2 (SomeDate,batchNumber) VALUES (GETDATE(),@BatchCounter)
    SET @LocalCounter +=1
    END

SET @LocalCounter = 0

    WHILE (@LocalCounter <= @NumRows)
    BEGIN
    INSERT TestInt (SomeDate,batchNumber) VALUES (GETDATE(),@BatchCounter)
    SET @LocalCounter +=1
    END

SET @BatchCounter +=1
COMMIT 
END

DBCC showcontig ('TestGuid2')  WITH tableresults
DBCC showcontig ('TestInt')  WITH tableresults

SELECT batchNumber,DATEDIFF(ms,MIN(SomeDate),MAX(SomeDate)) AS [NEWSEQUENTIALID()]
FROM TestGuid2
GROUP BY batchNumber

SELECT batchNumber,DATEDIFF(ms,MIN(SomeDate),MAX(SomeDate)) AS [IDENTITY()]
FROM TestInt
GROUP BY batchNumber

DROP TABLE TestGuid2
DROP TABLE TestInt

更新: 修改脚本以基于TEMP表执行插入,如下面的Phil Sandler,Mitch Wheat和Martin的示例中所示,我还发现IDENTITY的速度更快。但这不是传统的插入行的方式,而且我仍然不明白为什么实验起初会出错:即使我从原始示例中省略了GETDATE(),IDENTITY()仍然要慢得多。因此,似乎使IDENTITY()胜过NEWSEQUENTIALID()的唯一方法是准备要插入到临时表中的行,并使用此临时表以批量插入的方式执行许多插入。总而言之,我认为我们还没有找到这种现象的解释,而且IDENTITY()对于大多数实际用法似乎仍然较慢。谁能解释一下?


4
只是想一想:生成新的GUID可以完全不涉及表而进行,而获取下一个可用的标识值会暂时引入某种锁以确保两个线程/连接不会获得相同的值吗?我只是在猜。有趣的问题!
生气的人

4
谁说他们这样做?有很多证据表明他们没有这样做-看到Kimberly Tripp的磁盘空间很便宜-这不是重点!博客文章-她做了相当广泛的检讨,和GUID永远失去了清楚地INT IDENTITY
marc_s

2
好吧,上面的实验显示了相反的结果,并且结果是可重复的。
someName 2011年

2
使用IDENTITY不需要表锁。从概念上讲,我可以看到您可能期望它采用MAX(id)+ 1,但实际上存储了下一个值。实际上,它应该比查找下一个GUID更快。

4
另外,大概TestGuid2表的填充列应为CHAR(88)以使行大小相等
Mitch Wheat

Answers:


19

我修改了@Phil Sandler的代码,以消除调用GETDATE()的影响(可能涉及硬件影响/中断?),并使行的长度相同。

[自SQL Server 2000以来,有几篇文章涉及计时问题和高分辨率计时器,因此,我希望将这种影响降至最低。]

在具有数据和日志文件的简单恢复模型中,这两种方法都可以根据所需大小进行调整,以下是时间安排(以秒为单位):(根据下面的确切代码更新了新结果)

       Identity(s)  Guid(s)
       ---------    -----
       2.876        4.060    
       2.570        4.116    
       2.513        3.786   
       2.517        4.173    
       2.410        3.610    
       2.566        3.726
       2.376        3.740
       2.333        3.833
       2.416        3.700
       2.413        3.603
       2.910        4.126
       2.403        3.973
       2.423        3.653
    -----------------------
Avg    2.650        3.857
StdDev 0.227        0.204

使用的代码:

SET NOCOUNT ON

CREATE TABLE TestGuid2 (Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(88))

CREATE TABLE TestInt (Id Int NOT NULL identity(1,1) PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

DECLARE @Numrows INT = 1000000

CREATE TABLE #temp (Id int NOT NULL Identity(1,1) PRIMARY KEY, rowNum int, adate datetime)

DECLARE @LocalCounter INT = 0

--put rows into temp table
WHILE (@LocalCounter < @NumRows)
BEGIN
    INSERT INTO #temp(rowNum, adate) VALUES (@LocalCounter, GETDATE())
    SET @LocalCounter += 1
END

--Do inserts using GUIDs
DECLARE @GUIDTimeStart DateTime = GETDATE()
INSERT INTO TestGuid2 (SomeDate, batchNumber) 
SELECT adate, rowNum FROM #temp
DECLARE @GUIDTimeEnd  DateTime = GETDATE()

--Do inserts using IDENTITY
DECLARE @IdTimeStart DateTime = GETDATE()
INSERT INTO TestInt (SomeDate, batchNumber) 
SELECT adate, rowNum FROM #temp
DECLARE @IdTimeEnd DateTime = GETDATE()

SELECT DATEDIFF(ms, @IdTimeStart, @IdTimeEnd) AS IdTime, DATEDIFF(ms, @GUIDTimeStart, @GUIDTimeEnd) AS GuidTime

DROP TABLE TestGuid2
DROP TABLE TestInt
DROP TABLE #temp
GO

阅读@Martin的调查后,我在两种情况下都使用了建议的TOP(@num),即

...
--Do inserts using GUIDs
DECLARE @num INT = 2147483647; 
DECLARE @GUIDTimeStart DATETIME = GETDATE(); 
INSERT INTO TestGuid2 (SomeDate, batchNumber) 
SELECT TOP(@num) adate, rowNum FROM #temp; 
DECLARE @GUIDTimeEnd DATETIME = GETDATE();

--Do inserts using IDENTITY
DECLARE @IdTimeStart DateTime = GETDATE()
INSERT INTO TestInt (SomeDate, batchNumber) 
SELECT TOP(@num) adate, rowNum FROM #temp;
DECLARE @IdTimeEnd DateTime = GETDATE()
...

计时结果如下:

       Identity(s)  Guid(s)
       ---------    -----
       2.436        2.656
       2.940        2.716
       2.506        2.633
       2.380        2.643
       2.476        2.656
       2.846        2.670
       2.940        2.913
       2.453        2.653
       2.446        2.616
       2.986        2.683
       2.406        2.640
       2.460        2.650
       2.416        2.720

    -----------------------
Avg    2.426        2.688
StdDev 0.010        0.032

由于查询从未返回,因此我无法获得实际的执行计划!似乎有可能存在错误。(运行Microsoft SQL Server 2008 R2(RTM)-10.50.1600.1(X64))


7
整洁地说明良好基准测试的关键要素:确保您一次只测量一件事。
亚伦诺特2011年

你来这里有什么计划?它有SORTGUID 的运算符吗?
马丁·史密斯

@马丁:嗨,我没有检查计划(一次做几件事:))。我待会儿再看...
Mitch Wheat

@Mitch-对此有任何反馈吗?我相当怀疑您要在此处测量的主要内容是对大型插入物的引导进行排序所花费的时间,尽管有趣的是不能回答OP的原始问题,该问题是要解释为什么顺序引导在单个插入时的性能优于标识列行插入到OP的测试中。
马丁·史密斯

2
@Mitch-尽管我思考的越多,我对为什么有人仍然要使用的理解就越少NEWSEQUENTIALID。这将使索引更深,在OP的情况下使用更多的数据页,并且只有在重新引导计算机之前,它才能保证不断增加,因此与相比有很多缺点identity。在这种情况下,查询计划似乎又增加了一个不必要的查询!
马丁·史密斯

19

在简单恢复模式下的新数据库中,数据文件大小为1GB,日志文件大小为3GB(便携式计算机,两个文件都在同一驱动器上),恢复间隔设置为100分钟(以避免检查点使结果倾斜)单行结果与您相似inserts

我测试了三种情况:对于每种情况,我做了20批,分别向下表插入了100,000行。完整的脚本可以在该答案的修订历史中找到

CREATE TABLE TestGuid
  (
     Id          UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
     SomeDate    DATETIME, batchNumber BIGINT, FILLER CHAR(100)
  )

CREATE TABLE TestId
  (
     Id          Int NOT NULL identity(1, 1) PRIMARY KEY,
     SomeDate    DATETIME, batchNumber BIGINT, FILLER CHAR(100)
  )

CREATE TABLE TestInt
  (
     Id          Int NOT NULL PRIMARY KEY,
     SomeDate    DATETIME, batchNumber BIGINT, FILLER  CHAR(100)
  )  

对于第三张表,测试插入的行具有递增Id值,但这是通过在循环中递增变量的值自行计算的。

平均20个批次的时间得出以下结果。

NEWSEQUENTIALID() IDENTITY()  INT
----------------- ----------- -----------
1999              2633        1878

结论

因此,肯定identity是负责结果的创建过程的开销。对于自行计算的递增整数,则结果与仅考虑IO成本时所期望的结果更加一致。

当我将上述插入代码放入存储过程中并进行检查时sys.dm_exec_procedure_stats,将得到以下结果

proc_name      execution_count      total_worker_time    last_worker_time     min_worker_time      max_worker_time      total_elapsed_time   last_elapsed_time    min_elapsed_time     max_elapsed_time     total_physical_reads last_physical_reads  min_physical_reads   max_physical_reads   total_logical_writes last_logical_writes  min_logical_writes   max_logical_writes   total_logical_reads  last_logical_reads   min_logical_reads    max_logical_reads
-------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- --------------------
IdentityInsert 20                   45060360             2231067              2094063              2645079              45119362             2234067              2094063              2660080              0                    0                    0                    0                    32505                1626                 1621                 1626                 6268917              315377               276833               315381
GuidInsert     20                   34829052             1742052              1696051              1833055              34900053             1744052              1698051              1838055              0                    0                    0                    0                    35408                1771                 1768                 1772                 6316837              316766               298386               316774

因此,这些结果total_worker_time要高出约30%。这代表

自存储过程被编译以来,执行该存储过程所消耗的CPU总时间(以微秒为单位)。

因此,看起来好像生成该IDENTITY值的代码比生成该代码的代码要NEWSEQUENTIALID()占用更多的CPU资源(两个数字之间的差是10231308,平均每个插入大约5µs。)并且对于此表定义,这是固定的CPU成本足够高,足以抵消由于键的宽度较大而导致的其他逻辑读取和写入。(注意:Itzik Ben Gan在此处进行了类似的测试,发现每个插入片段会造成2µs的损失)

那么,为什么IDENTITYCPU占用更多的资源UuidCreateSequential呢?

我相信是这样解释这篇文章。对于identity生成的每十分之一的值,SQL Server必须将更改写入磁盘上的系统表

那么多排刀片呢?

当在一条语句中插入100,000行时,我发现差异消失了,对这种GUID情况可能仍然有一点好处,但是没有明显的结果。我的测试中20批次的平均值为

NEWSEQUENTIALID() IDENTITY()
----------------- -----------
1016              1088

在Phil的代码和Mitch的第一组结果中没有明显的损失的原因是因为碰巧我用来执行多行插入的代码SELECT TOP (@NumRows)。这使优化器无法正确估计将要插入的行数。

这似乎是有益的,因为在某个转折点处,它将为(假定是顺序的!)添加一个附加的排序操作GUID

GUID排序

BOL中的说明文字不需要这种排序操作。

创建一个大于自Windows启动以来在指定计算机上此功能先前生成的GUID的GUID。重新启动Windows后,GUID可以从较低的范围再次启动,但是仍然是全局唯一的。

因此,在我看来,SQL Server无法识别计算标量的输出已经像在该identity列中一样已经进行了预排序,因此似乎存在错误或缺少优化。(编辑,我报告了这个,现在不必要的排序问题已在Denali中修复


不,它有一大堆只是为了清楚起见丹尼引用数,20个缓存标识值,是不正确的影响,但-它应该是10
阿龙贝特朗

@AaronBertrand-谢谢。您链接的那篇文章内容最丰富。
马丁·史密斯,

8

非常简单:使用GUID,在行中生成下一个数字比使用IDENTITY便宜(GUID的当前值不必存储,IDENTITY必须存储)。即使对于NEWSEQUENTIALGUID也是如此。

您可以使测试更公平,并使用带有较大CACHE的SEQUENCER-比IDENTITY便宜。

但是正如MR所说,GUID有一些主要优点。实际上,它们比IDENTITY列具有更大的可伸缩性(但前提是它们不是顺序的)。

参见:http : //blog.kejser.org/2011/10/05/boosting-insert-speed-by-generating-scalable-keys/


我认为您想念他们正在使用顺序向导。
马丁·史密斯

马丁:该论点对于顺序GUID也适用。必须存储IDENTITY(在重新启动后返回其旧值),顺序GUID没有此限制。
Thomas Kejser 2014年

2
是的,在我发表评论后,您意识到正在谈论持久存储而不是存储在内存中。2012年确实也使用了缓存IDENTITY因此在这里抱怨
Martin Smith

4

我对这种类型的问题着迷。为什么您必须在星期五晚上发布它?:)

我认为,即使您的测试仅旨在测量INSERT性能,您(可能)仍会引入许多可能会引起误解的因素(循环,长时间运行的事务等)。

我并不完全相信我的版本可以证明任何内容,但是身份确实比其中的GUID表现更好(3.2秒对家用PC上的6.8秒):

SET NOCOUNT ON

CREATE TABLE TestGuid2 (Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

CREATE TABLE TestInt (Id Int NOT NULL identity(1,1) PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

DECLARE @Numrows INT = 1000000

CREATE TABLE #temp (Id int NOT NULL Identity(1,1) PRIMARY KEY, rowNum int)

DECLARE @LocalCounter INT = 0

--put rows into temp table
WHILE (@LocalCounter < @NumRows)
BEGIN
    INSERT INTO #temp(rowNum) VALUES (@LocalCounter)
    SET @LocalCounter += 1
END

--Do inserts using GUIDs
DECLARE @GUIDTimeStart DateTime = GETDATE()
INSERT INTO TestGuid2 (SomeDate, batchNumber) 
SELECT GETDATE(), rowNum FROM #temp
DECLARE @GUIDTimeEnd  DateTime = GETDATE()

--Do inserts using IDENTITY
DECLARE @IdTimeStart DateTime = GETDATE()
INSERT INTO TestInt (SomeDate, batchNumber) 
SELECT GETDATE(), rowNum FROM #temp
DECLARE @IdTimeEnd DateTime = GETDATE()

SELECT DATEDIFF(ms, @IdTimeStart, @IdTimeEnd) AS IdTime
SELECT DATEDIFF(ms, @GUIDTimeStart, @GUIDTimeEnd) AS GuidTime

DROP TABLE TestGuid2
DROP TABLE TestInt
DROP TABLE #temp

没有人提到的另一个因素是数据库恢复模型和日志文件的增长……
米奇·

在简单恢复模式下的新数据库上,@ Mitch的数据和日志文件大小都超出了所需的大小,我得到了与OP类似的结果。
马丁·史密斯

我刚刚获得了2.560秒(身份)和3.666秒(Guid)的时间(在简单的恢复模型中,数据和日志文件的大小都超出了所需的大小)
米奇·

@Mitch-在OP的代码中同时进行所有事务还是在Phil的代码中?
马丁·史密斯

在此海报代码上,这就是为什么我在这里评论。我还发布了我使用的代码...
米奇·麦特

3

我多次运行您的示例脚本,对批计数和大小进行了一些调整(非常感谢您提供它)。

首先,我要说的是,您只测量按键性能的一个方面- INSERT速度。因此,除非您特别关心只将数据尽快放入表中,否则这种动物将面临更多的挑战。

我的发现总体上与您相似。然而,我要提及在于方差INSERT速度之间GUIDIDENTITY(INT)是稍微较大的GUIDIDENTITY-说不定+/-运行之间10%。IDENTITY每次使用的批次变化小于2-3%。

还需要注意的是,我的测试框显然不如您的强大,因此我不得不使用较小的行数。


当PK是GUID时,引擎是否可能不使用索引而是使用哈希算法来确定相应记录的物理位置?由于没有索引开销,因此插入具有哈希主键的稀疏表中的插入总是比插入具有主键索引的表中的插入要快。这只是一个问题-如果答案是否定的,请不要拒绝我。只需提供指向权威的链接即可。

1

我将再次参考关于同一主题在stackoverflow上的另一个转换-https: //stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequential-guid-over-standard-guid

我确实知道的一件事是,具有顺序GUID的原因是,由于叶子移动很少,索引使用效果更好,因此减少了HD寻道。因此,我认为插入也将更快,因为它不必将密钥分布在大量页面上。

我的个人经验是,当您实现大型高流量数据库时,最好使用GUID,因为它使它与其他系统集成时具有更大的可伸缩性。特别是对于复制和int / bigint的限制。...并不是您会用完bigints,但最终您会循环使用。


1
您不会用完BIGINT,永远不会...参见:sqlmag.com/blog/it-possible-run-out-bigint-values
Thomas Kejser 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.