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?
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))
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
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)
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
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!) GUID
s hinzugefügt wird.
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.)
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/
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
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.
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.