テストケースでシーケンシャルGUIDキーがシーケンシャルINTキーよりも高速に実行されるのはなぜですか?


39

求めた後、このシーケンシャルおよび非シーケンシャルGUIDを比較する質問を、私はGUID主キーを持つテーブルがで順次初期化)1上のINSERTのパフォーマンスを比較してみましたnewsequentialid()主キーがで順次初期化INTと、および2)テーブル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

更新: 以下のPhil Sandler、Mitch Wheat、Martinの例のように、TEMPテーブルに基づいて挿入を実行するようにスクリプトを変更すると、IDENTITYのほうが高速になります。しかし、これは行を挿入する従来の方法ではなく、最初に実験が失敗した理由はまだわかりません。元の例からGETDATE()を省略しても、IDENTITY()の方がずっと遅いです。したがって、IDENTITY()がNEWSEQUENTIALID()よりも優れている唯一の方法は、一時テーブルに挿入する行を準備し、この一時テーブルを使用してバッチ挿入として多くの挿入を実行することです。全体として、私は現象の説明を見つけたとは思わず、IDENTITY()はほとんどの実用的な使用法でまだ遅いようです。誰でもこれを説明できますか?


4
考えてみてください:新しいGUIDの生成は、テーブルをまったく使用せずに実行できますが、次に利用可能なID値を取得すると、2つのスレッド/接続が同じ値を取得しないように一時的に何らかのロックが導入されますか?私は本当に推測しています。興味深い質問です!
怒っている人

4
誰が言うの?彼らにはない多くの証拠があります-Kimberly Trippのディスク容量が安いのを見てください-それはポイントではありませんブログ記事-彼女はかなり大規模な見直しを行い、およびGUIDが常にに明確に失うINT IDENTITY
marc_s

2
さて、上記の実験は逆を示しており、結果は再現可能です。
-someName

2
を使用するにIDENTITYは、テーブルロックは必要ありません。概念的には、MAX(id)+ 1をとると予想されるかもしれませんが、実際には次の値が格納されます。実際には、次のGUIDを見つけるよりも速いはずです。

4
また、おそらくTestGuid2テーブルの充填カラムは、行のサイズを等しくするためにCHAR(88)でなければならない
ミッチ小麦

回答:


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
適切なベンチマークの重要な要素をきちんと示しています。一度に1つのものだけを測定していることを確認してください。
アーロンノート

ここでどのような計画を立てますか?それは持っていますSORTGUIDの演算子を?
マーティンスミス

@マーティン:こんにちは、私は計画を確認しませんでした(一度にいくつかのことを行います:))。私は...少し後で見てよ
ミッチ小麦

@Mitch-これに関するフィードバックはありますか?ここであなたが測定している主なものは、大きな挿入物のGUIDをソートするのにかかる時間であると思いますOPのテストでの行の挿入。
マーティンスミス

2
@Mitch-それについて考えれば考えるほど、だれかがNEWSEQUENTIALIDとにかく使用したいと思う理由がわかりません。インデックスをより深くし、OPの場合は20%以上のデータページを使用し、マシンが再起動されるまで増加し続けることが保証されているため、に対して多くの欠点がありidentityます。この場合、クエリプランによってさらに不必要なプランが追加されるようです。
マーティンスミス

19

データファイルのサイズが1GBでログファイルが3GB(ラップトップマシン、同じドライブ上の両方のファイル)で、復旧間隔が100分に設定された単純な復旧モデルの新しいデータベースで(チェックポイントによる結果のずれを避けるため)単一の行であなたと同様の結果inserts

3つのケースをテストしました。それぞれのケースで、次のテーブルに100,000行を個別に挿入する20バッチを実行しました。完全なスクリプトは、この回答の改訂履歴に記載されています。

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)
  )  

3番目のテーブルでは、テストで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値を生成するコードは、生成するコードよりもCPUを集中的に使用しているように見えますNEWSEQUENTIALID()(2つの数字の差は、挿入ごとに平均5µsで平均される10231308です)。キーの幅が広いために発生する追加の論理読み取りおよび書き込みを上回るのに十分な大きさでした。(注:Itzik Ben Ganは同様のテストをここで行い、挿入ごとに2µsのペナルティを見つけました)

では、なぜIDENTITYCPUよりもCPUに負荷がかかるのUuidCreateSequentialでしょうか?

これはこの記事で説明さていると思います。identitySQL Serverは、生成された10番目の値ごとに、ディスク上のシステムテーブルに変更を書き込む必要があります

MultiRow Insertsはどうですか?

100,000行が1つのステートメントに挿入されたとき、違いは解消されたが、おそらくGUIDケースにわずかな利点がありますが、明確な結果にはほど遠いことがわかりました。私のテストでの20バッチの平均は

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

PhilのコードとMitchの最初の結果セットに明らかなペナルティがない理由は、複数行挿入を行うために使用したコードが使用されたためですSELECT TOP (@NumRows)。これにより、オプティマイザーは挿入される行数を正しく推定できませんでした。

これは、(おそらく順次!)に追加のソート操作を追加する特定の転換点があるため、有益であると思われますGUID

GUIDソート

このソート操作は、BOLの説明テキストから必要ありません。

Windowsの起動後に指定されたコンピューターでこの関数によって以前に生成されたGUIDよりも大きいGUIDを作成します。Windowsを再起動すると、GUIDはより低い範囲から再開できますが、それでもグローバルに一意です。

そのため、SQL Serverは、identity列に対して明らかに行われているように、計算スカラーの出力が既に事前に並べ替えられていることを認識していないというバグまたは最適化の欠如を感じました。(編集私はこれを報告し、不要なソートの問題はデナリで修正されました


それはインパクトの全体の多くを持っていますが、単に明瞭化のためにデニーが引用された回数、20のキャッシュされたID値は、間違っていないこと-それは10でなければなりません
アーロン・ベルトラン

@AaronBertrand-ありがとう。あなたがリンクした記事は最も有益です。
マーティンスミス

8

非常に簡単:GUIDを使用すると、IDENTITYの場合よりも行の次の番号を生成する方が安価です(GUIDの現在の値を保存する必要はなく、IDENTITYを保存する必要があります)。これはNEWSEQUENTIALGUIDにも当てはまります。

テストをより公正にし、IDENTITYよりも安価な大きなCACHEを備えたSEQUENCERを使用できます。

しかし、MRが言うように、GUIDにはいくつかの大きな利点があります。実際のところ、IDENTITYカラムよりもはるかにスケーラブルです(ただし、シーケンシャルでない場合のみ)。

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


彼らはシーケンシャルGUIDを使用していることを見逃したと思う。
マーティンスミス14年

Martin:引数はシーケンシャルGUIDにも当てはまります。IDENTITYを保存する必要があり(再起動後に元の値に戻すため)、シーケンシャルGUIDにはこの制限がありません。
トーマスケイサー14年

2
はい、私のコメントの後、メモリに保存するのではなく、永続的に保存することについて話していました。IDENTITYただし、2012 も同様にキャッシュを使用します。したがって、ここで苦情
マーティンスミス14年

4

私はこのタイプの質問に魅了されています。金曜日の夜に投稿しなければならなかったのはなぜですか?:)

テストがINSERTパフォーマンスの測定のみを目的としている場合でも、誤解を招く可能性のある多くの要因(ループ、長時間実行されるトランザクションなど)を導入した可能性があります

私のバージョンが何かを証明していると完全に確信しているわけではありませんが、アイデンティティはその中のGUIDよりも優れています(自宅のPCでは3.2秒対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と同様の結果が得られます。
マーティンスミス

Identityで2.560秒、Guidで3.666秒のタイミングを取得しました(データとログファイルが両方とも必要なサイズを超える単純な復旧モデルで)
Mitch Wheat

@Mitch-同じトランザクション内のOPのコードまたはPhilのコード
マーティンスミス

このポスターのコードで、私がここでコメントしているのはそのためです。私も...私が使用したコードを掲載しました
ミッチ小麦

3

サンプルスクリプトを数回実行して、バッチカウントとサイズを調整しました(そして、提供してくれてありがとう)。

最初に、キーのパフォーマンスの1つの側面であるINSERT速度だけを測定していると言います。したがって、できるだけ早くテーブルにデータを取り込むことだけに特に関心がない限り、この動物にはさらに多くのものがあります。

私の発見は一般的にあなたのものに似ていました。ただし、と(int)のINSERT間の速度のばらつきは、- との場合よりもわずかに大きくなります-実行間で+/- 10%になる可能性があります。使用したバッチの変動は、毎回2〜3%未満でした。GUIDIDENTITYGUIDIDENTITYIDENTITY

また、私のテストボックスは明らかにあなたのテストボックスよりも性能が低いため、より少ない行数を使用する必要がありました。


PKがGUIDの場合、エンジンがインデックスではなくハッシュアルゴリズムを使用して、対応するレコードの物理的な場所を特定することは可能ですか?ハッシュされた主キーを持つスパーステーブルへの挿入は、インデックスオーバーヘッドがないため、主キーにインデックスがあるテーブルへの挿入よりも常に高速です。これは単なる質問です。答えが「いいえ」の場合、私に投票しないでください。当局へのリンクを提供してください。

1

私はこの同じトピックのためのstackoverflow上の別のコンバージョンに戻って参照するつもりだ- https://stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequential-guid-over-standard-guid

私が知っていることの1つは、シーケンシャルGUIDを使用すると、リーフの動きがほとんどないため、インデックスの使用が改善され、HDシークが減少することです。このため、多数のページにキーを配布する必要がないため、挿入も高速になると思います。

私の個人的な経験では、大規模でトラフィックの多いDBを実装するときは、GUIDを使用する方が良いと言えます。GUIDを使用すると、他のシステムとの統合がはるかにスケーラブルになります。具体的には、レプリケーション、およびint / bigintの制限について説明します。...bigintを使い果たすことはありませんが、最終的には循環するようになります。


1
:あなたは決して...これを参照してください、BIGINTsが不足しないsqlmag.com/blog/it-possible-run-out-bigint-valuesを
トーマスKejser
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.