it-swarm-eu.dev

Warum arbeiten sequentielle GUID - Schlüssel in meinem Testfall schneller als sequentielle INT-Schlüssel?

Nachdem ich this Frage zum Vergleich von sequentiellen und nicht-sequentiellen GUIDs gestellt hatte, versuchte ich, die INSERT-Leistung für 1) eine Tabelle mit einem GUID Primärschlüssel, der nacheinander mit newsequentialid() und 2) eine Tabelle mit einem INT-Primärschlüssel, der nacheinander mit identity(1,1) initialisiert wird. Ich würde erwarten, dass Letzteres aufgrund der geringeren Breite von Ganzzahlen am schnellsten ist, und es scheint auch einfacher zu sein, eine sequentielle Ganzzahl als eine sequentielle GUID zu generieren. Zu meiner Überraschung waren INSERTs in der Tabelle mit dem Integer-Schlüssel erheblich langsamer als die sequentielle Tabelle GUID).

Dies zeigt die durchschnittliche Zeitnutzung (ms) für die Testläufe:

NEWSEQUENTIALID()  1977
IDENTITY()         2223

Kann jemand das erklären?

Das folgende Experiment wurde verwendet:

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: Ändern des Skripts, um die Einfügungen basierend auf einer TEMP-Tabelle durchzuführen, wie in den Beispielen von Phil Sandler, Mitch Wheat und Martin unten, ich auch Finden Sie heraus, dass IDENTITY schneller ist, als es sein sollte. Dies ist jedoch nicht die herkömmliche Methode zum Einfügen von Zeilen, und ich verstehe immer noch nicht, warum das Experiment zunächst schief gelaufen ist: Auch wenn ich GETDATE () in meinem ursprünglichen Beispiel weglasse, ist IDENTITY () immer noch viel langsamer. Es scheint also, dass die einzige Möglichkeit, IDENTITY () NEWSEQUENTIALID () zu übertreffen, darin besteht, die Zeilen für das Einfügen in eine temporäre Tabelle vorzubereiten und die vielen Einfügungen als Batch-Einfügung unter Verwendung dieser temporären Tabelle durchzuführen. Alles in allem glaube ich nicht, dass wir eine Erklärung für das Phänomen gefunden haben, und IDENTITY () scheint für die meisten praktischen Anwendungen immer noch langsamer zu sein. Kann jemand das erklären?

39
someName

Ich habe den Code von @Phil Sandler geändert, um den Effekt des Aufrufs von GETDATE () zu entfernen (möglicherweise sind Hardwareeffekte/Interrupts beteiligt?) Und Zeilen mit derselben Länge erstellt.

[Seit SQL Server 2000 gab es mehrere Artikel zu Zeitproblemen und hochauflösenden Zeitgebern, daher wollte ich diesen Effekt minimieren.]

In einem einfachen Wiederherstellungsmodell mit Daten und Protokolldateien, die beide die erforderliche Größe haben, sind hier die Zeitangaben (in Sekunden) aufgeführt: (Aktualisiert mit neuen Ergebnissen basierend auf dem genauen Code unten)

       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

Der verwendete Code:

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

Nachdem ich @ Martins Untersuchung gelesen hatte, lief ich in beiden Fällen erneut mit dem vorgeschlagenen TOP (@num), d. H.

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

und hier sind die Timing-Ergebnisse:

       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

Ich konnte den tatsächlichen Ausführungsplan nicht abrufen, da die Abfrage nie zurückgegeben wurde! Es scheint, dass ein Fehler wahrscheinlich ist. (Ausführen von Microsoft SQL Server 2008 R2 (RTM) - 10.50.1600.1 (X64))

19
Mitch Wheat

Auf einer neuen Datenbank in einem einfachen Wiederherstellungsmodell mit einer Datendateigröße von 1 GB und einer Protokolldatei von 3 GB (Laptop, beide Dateien auf demselben Laufwerk) und einem Wiederherstellungsintervall von 100 Minuten (um zu vermeiden, dass ein Prüfpunkt die Ergebnisse verzerrt) sehe ich ähnliche Ergebnisse wie bei der einzelnen Zeile inserts.

Ich habe drei Fälle getestet: Für jeden Fall habe ich 20 Stapel von 100.000 Zeilen einzeln in die folgenden Tabellen eingefügt. Die vollständigen Skripte finden Sie im Revisionsverlauf dieser Antwort .

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

Für die dritte Tabelle fügte der Test Zeilen mit einem inkrementierenden Id -Wert ein, der jedoch durch Inkrementieren des Werts einer Variablen in einer Schleife selbst berechnet wurde.

Die Mittelung der Zeit, die über die 20 Chargen benötigt wurde, ergab die folgenden Ergebnisse.

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

Fazit

Es scheint also definitiv ein Overhead des Erstellungsprozesses von identity zu sein, der für die Ergebnisse verantwortlich ist. Für die selbst berechnete inkrementierende Ganzzahl stimmen die Ergebnisse viel besser mit den erwarteten Ergebnissen überein, wenn nur die Kosten IO) berücksichtigt werden.

Wenn ich den oben beschriebenen Einfügecode in gespeicherte Prozeduren einfüge und sys.dm_exec_procedure_stats Überprüfe, werden die folgenden Ergebnisse angezeigt

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

In diesen Ergebnissen ist total_worker_time Etwa 30% höher. Dies stellt dar

Gesamtmenge an CPU-Zeit in Mikrosekunden, die durch die Ausführung dieser gespeicherten Prozedur seit ihrer Kompilierung verbraucht wurde.

Es sieht also einfach so aus, als ob der Code, der den Wert IDENTITY generiert, CPU-intensiver ist als der Code, der die NEWSEQUENTIALID() generiert Einfügen.) und dass für diese Tabellendefinition diese festen CPU-Kosten ausreichend hoch waren, um die zusätzlichen logischen Lese- und Schreibvorgänge zu überwiegen, die aufgrund der größeren Breite des Schlüssels anfallen. (NB: Itzik Ben Gan hat ähnliche Tests hier und eine Strafe von 2µs pro Einsatz gefunden)

Warum ist IDENTITY CPU-intensiver als UuidCreateSequential?

Ich glaube das wird erklärt in diesem Artikel . Für jeden zehnten generierten identity -Wert muss SQL Server die Änderung in die Systemtabellen auf der Festplatte schreiben

Was ist mit MultiRow-Einsätzen?

Als die 100.000 Zeilen in eine einzelne Anweisung eingefügt wurden, stellte ich fest, dass der Unterschied verschwunden war, was für den Fall GUID vielleicht noch einen leichten Vorteil hatte, aber bei weitem nicht so eindeutig war. Der Durchschnitt für 20 Chargen in meinem Test war

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

Der Grund dafür, dass die Strafe in Phils Code und in Mitch 'erstem Ergebnissatz nicht erkennbar ist, ist, dass der Code, den ich für die mehrzeilige Einfügung verwendet habe, SELECT TOP (@NumRows) verwendet hat. Dies verhinderte, dass der Optimierer die Anzahl der einzufügenden Zeilen korrekt schätzte.

Dies scheint von Vorteil zu sein, da es einen bestimmten Wendepunkt gibt, an dem eine zusätzliche Sortieroperation für die (angeblich sequentiellen!) GUIDs hinzugefügt wird.

GUID Sort

Diese Sortieroperation ist ab dem erläuternden Text in BOL nicht erforderlich.

Erstellt eine GUID, die größer ist als jede GUID, die zuvor von dieser Funktion auf einem bestimmten Computer seit dem Start von Windows generiert wurde. Nach dem Neustart von Windows wird die GUID kann wieder in einem niedrigeren Bereich beginnen, ist aber immer noch global eindeutig.

Es schien mir also ein Fehler oder eine fehlende Optimierung zu sein, dass SQL Server nicht erkennt, dass die Ausgabe des Berechnungsskalars bereits vorsortiert ist, wie dies anscheinend bereits für die Spalte identity der Fall ist. ( Bearbeiten Ich habe dies gemeldet und das unnötige Sortierproblem wurde jetzt in Denali behoben.)

19
Martin Smith

Ganz einfach: Mit GUID ist es billiger, die nächste Zahl in der Zeile zu generieren als mit IDENTITY (Der aktuelle Wert von GUID muss nicht gespeichert werden, die IDENTITY muss sein) Dies gilt auch für NEWSEQUENTIALGUID.

Sie könnten den Test fairer gestalten und einen SEQUENCER mit einem großen CACHE verwenden - was billiger als IDENTITY ist.

Aber wie M. R. sagt, haben GUIDs einige große Vorteile. Tatsächlich sind sie VIEL skalierbarer als IDENTITY-Spalten (aber nur, wenn sie NICHT sequentiell sind).

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

8
Thomas Kejser

Diese Art von Frage fasziniert mich. Warum musstest du es an einem Freitagabend posten? :) :)

Ich denke, selbst wenn Ihr Test NUR zur Messung der INSERT-Leistung gedacht ist, haben Sie (möglicherweise) eine Reihe von Faktoren eingeführt, die irreführend sein können (Schleifen, eine lang laufende Transaktion usw.).

Ich bin nicht ganz davon überzeugt, dass meine Version etwas beweist, aber die Identität ist besser als die darin enthaltenen GUIDs (3,2 Sekunden gegenüber 6,8 Sekunden auf einem Heim-PC):

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

Ich habe Ihr Beispielskript mehrmals ausgeführt und einige Änderungen an der Anzahl und Größe der Stapel vorgenommen (und vielen Dank, dass Sie es bereitgestellt haben).

Zuerst sage ich, dass Sie nur einen Aspekt der Leistung der Tasten messen - INSERT Geschwindigkeit. Wenn Sie sich also nicht speziell darum kümmern, Daten so schnell wie möglich in die Tabellen zu bringen, steckt in diesem Tier noch viel mehr.

Meine Ergebnisse waren im Allgemeinen Ihren ähnlich. Ich würde jedoch erwähnen, dass die Varianz in der Geschwindigkeit von INSERT zwischen GUID und IDENTITY (int) geringfügig größer mit GUID als ist mit IDENTITY - vielleicht +/- 10% zwischen den Läufen. Die Chargen, die IDENTITY verwendeten, variierten jedes Mal weniger als 2 - 3%.

Außerdem ist meine Testbox deutlich weniger leistungsstark als Ihre, sodass ich kleinere Zeilenzahlen verwenden musste.

3
Yuck

Ich werde für dasselbe Thema auf eine andere Conv on Stackoverflow zurückgreifen - https://stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequential-guid-over -standard-guid

Eine Sache, die ich weiß, ist, dass sequentielle GUIDs darin bestehen, dass die Indexnutzung aufgrund sehr geringer Blattbewegungen besser ist und daher die HD-Suche reduziert wird. Aus diesem Grund würden die Einfügungen auch schneller sein, da die Schlüssel nicht auf eine große Anzahl von Seiten verteilt werden müssen.

Meine persönliche Erfahrung ist, dass es bei der Implementierung einer großen Datenbank mit hohem Datenverkehr besser ist, GUIDs zu verwenden, da diese für die Integration mit anderen Systemen viel skalierbarer sind. Dies gilt insbesondere für die Replikation und die Int/Bigint-Grenzwerte. Nicht, dass Ihnen die Bigint ausgehen würden, aber irgendwann werden Sie es tun und zurückfahren.

1
M.R.