web-dev-qa-db-ja.com

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

順次GUIDと非順次GUIDを比較する this 質問を尋ねた後、1)のINSERTパフォーマンスをGUIDプライマリキーをnewsequentialid()、および2)identity(1,1)で順次初期化されたINT主キーを持つテーブル。整数の幅が小さいため、後者が最も高速になると思います。また、順次GUIDよりも順次整数を生成する方が簡単なようです。しかし、驚いたことに、整数キーを使用したテーブルでのINSERTは、シーケンシャルGUIDテーブルよりも大幅に低速でした。

これは、テスト実行の平均時間使用量(ms)を示しています。

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

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

39
someName

@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)を実行)

19
Mitch Wheat

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値を生成するコードは、NEWSEQUENTIALID()を生成するコードよりもCPUに負荷がかかるように見えます(2つの数値の差は10231308で、平均して約5µsです)挿入)。このテーブル定義の場合、この固定CPUコストは、キーの幅が広いために発生した追加の論理読み取りおよび書き込みを上回るほど高額でした。 (注:Itzik Ben Ganは ここでも同様のテスト を行い、挿入ごとに2µsのペナルティを発見しました)

それでは、なぜIDENTITYUuidCreateSequentialよりもCPUに負荷がかかるのですか?

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

MultiRow挿入についてはどうですか?

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

_NEWSEQUENTIALID() IDENTITY()
----------------- -----------
1016              1088
_

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

(おそらく順次!)GUIDsに追加のソート操作を追加する特定の転換点があるため、これは利点のようです。

GUID Sort

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

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

したがって、SQL Serverがidentity列に対してすでにそうであるように、計算スカラーの出力がすでに事前に並べ替えられていることをSQL Serverが認識しないことがバグまたは最適化の欠如のように思えました。 (編集 これを報告し、不要なソートの問題がDenaliで修正されました

19
Martin Smith

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

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

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

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

8
Thomas Kejser

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

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

私のバージョンが何かを証明することを完全に確信しているわけではありませんが、IDはその中のGUIDよりも優れています(3.2秒vs自宅の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
4
Phil Sandler

サンプルスクリプトを数回実行して、バッチのカウントとサイズを調整しました(提供していただきありがとうございます)。

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

私の調査結果は一般的にあなたの調査結果と似ていました。ただし、INSERTGUID(int)の間のIDENTITY速度の差異はわずかより大きいであり、GUIDよりもwith IDENTITY-実行間でおそらく+/- 10% IDENTITYを使用したバッチの変動は、毎回2〜3%未満でした。

また、私のテストボックスは明らかにあなたのテストボックスよりも強力ではないので、小さい行数を使用する必要がありました。

3
Yuck

この同じトピックについて、stackoverflowの別のコンバージョンを参照します https://stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequential-guid-over -standard-guid

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

私の個人的な経験では、大規模な高トラフィックDBを実装する場合は、他のシステムとの統合のためのスケーラビリティが大幅に向上するため、GUIDを使用する方がよいということです。これは、特にレプリケーションとint/bigintの制限に当てはまります。bigintsが足りなくなるわけではありませんが、最終的にはサイクルバックします。

1
M.R.