Ich führe einen Leistungsvergleich zwischen der Verwendung von 1000 INSERT-Anweisungen durch:
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age)
VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0)
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age)
VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1)
...
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age)
VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999)
..versus mit einer einzelnen INSERT-Anweisung mit 1000 Werten:
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age)
VALUES
('db72b358-e9b5-4101-8d11-7d7ea3a0ae7d', 'First 0', 'Last 0', 0),
('6a4874ab-b6a3-4aa4-8ed4-a167ab21dd3d', 'First 1', 'Last 1', 1),
...
('9d7f2a58-7e57-4ed4-ba54-5e9e335fb56c', 'First 999', 'Last 999', 999)
Zu meiner großen Überraschung sind die Ergebnisse das Gegenteil von dem, was ich dachte:
- 1000 INSERT-Anweisungen: 290 ms.
- 1 INSERT-Anweisung mit 1000 VALUES: 2800 ms.
Der Test wird direkt in MSSQL Management Studio mit dem für die Messung verwendeten SQL Server Profiler ausgeführt (und ich habe ähnliche Ergebnisse beim Ausführen von C # -Code mit SqlClient, was angesichts aller DAL-Layer-Roundtrips noch überraschender ist).
Kann das vernünftig sein oder irgendwie erklärt werden? Wie kommt es, dass eine angeblich schnellere Methode zu einer zehnmal (!) Schlechteren Leistung führt?
Danke dir.
BEARBEITEN: Ausführungspläne für beide anhängen:
Antworten:
Ihr Plan zeigt, dass die einzelnen Einfügungen parametrisierte Prozeduren verwenden (möglicherweise automatisch parametrisiert), sodass die Analyse- / Kompilierungszeit für diese minimal sein sollte.
Ich dachte, ich würde das etwas genauer untersuchen, also richte eine Schleife (ein Skript ) ein und versuchte, die Anzahl der
VALUES
Klauseln anzupassen und die Kompilierungszeit aufzuzeichnen.Ich habe dann die Kompilierungszeit durch die Anzahl der Zeilen geteilt, um die durchschnittliche Kompilierungszeit pro Klausel zu erhalten. Die Ergebnisse sind unten
Bis zu 250
VALUES
Klauseln weisen die Kompilierungszeit / Anzahl der Klauseln einen leichten Aufwärtstrend auf, aber nichts zu dramatisches.Aber dann gibt es eine plötzliche Veränderung.
Dieser Abschnitt der Daten wird unten gezeigt.
Die Größe des zwischengespeicherten Plans, die linear gewachsen war, sinkt plötzlich, aber CompileTime erhöht sich um das 7-fache und CompileMemory schießt hoch. Dies ist der Grenzwert zwischen einem automatisch parametrisierten Plan (mit 1.000 Parametern) und einem nicht parametrisierten Plan. Danach scheint es linear weniger effizient zu werden (in Bezug auf die Anzahl der in einer bestimmten Zeit verarbeiteten Wertklauseln).
Ich bin mir nicht sicher, warum das so sein sollte. Vermutlich muss beim Erstellen eines Plans für bestimmte Literalwerte eine Aktivität ausgeführt werden, die nicht linear skaliert wird (z. B. Sortieren).
Es scheint keinen Einfluss auf die Größe des zwischengespeicherten Abfrageplans zu haben, als ich eine Abfrage ausprobiert habe, die vollständig aus doppelten Zeilen besteht, und es wirkt sich auch nicht auf die Reihenfolge der Ausgabe der Tabelle der Konstanten aus (und beim Einfügen in einen Heap-Zeitaufwand für das Sortieren wäre sowieso sinnlos, selbst wenn es so wäre).
Wenn der Tabelle ein Clustered-Index hinzugefügt wird, zeigt der Plan weiterhin einen expliziten Sortierschritt an, sodass er zur Kompilierungszeit nicht sortiert zu werden scheint, um eine Sortierung zur Laufzeit zu vermeiden.
Ich habe versucht, dies in einem Debugger zu überprüfen, aber die öffentlichen Symbole für meine Version von SQL Server 2008 scheinen nicht verfügbar zu sein. Stattdessen musste ich mir die entsprechende
UNION ALL
Konstruktion in SQL Server 2005 ansehen .Eine typische Stapelverfolgung finden Sie unten
Wenn Sie also die Namen in der Stapelverfolgung entfernen, scheint es viel Zeit zu kosten, Zeichenfolgen zu vergleichen.
Dieser KB-Artikel gibt an, dass dies
DeriveNormalizedGroupProperties
mit der sogenannten Normalisierungsphase der Abfrageverarbeitung verbunden istDiese Phase wird jetzt als Binden oder Algebrisieren bezeichnet. Sie verwendet die Ausgabe des Ausdrucksanalysebaums aus der vorherigen Analysestufe und gibt einen algebrisierten Ausdrucksbaum (Abfrageprozessorbaum) aus, um mit der Optimierung fortzufahren (in diesem Fall Trivialplanoptimierung) [ref] .
Ich habe ein weiteres Experiment ( Skript ) versucht , bei dem der ursprüngliche Test erneut ausgeführt wurde, wobei jedoch drei verschiedene Fälle untersucht wurden.
Es ist deutlich zu sehen, dass je länger die Saiten sind, desto schlechter werden die Dinge und umgekehrt, je mehr Duplikate, desto besser werden die Dinge. Wie bereits erwähnt, wirken sich Duplikate nicht auf die Größe des zwischengespeicherten Plans aus. Daher gehe ich davon aus, dass beim Erstellen des algebrisierten Ausdrucksbaums selbst ein Prozess zur Identifizierung von Duplikaten erforderlich ist.
Bearbeiten
Ein Ort, an dem diese Informationen genutzt werden , wird hier von @Lieven angezeigt
Da zur Kompilierungszeit festgestellt werden kann, dass die
Name
Spalte keine Duplikate enthält, wird die Reihenfolge nach dem sekundären1/ (ID - ID)
Ausdruck zur Laufzeit übersprungen (die Sortierung im Plan enthält nur eineORDER BY
Spalte) und es wird kein Fehler zum Teilen durch Null ausgelöst. Wenn der Tabelle Duplikate hinzugefügt werden, zeigt der Sortieroperator zwei Spalten an und der erwartete Fehler wird ausgelöst.quelle
<ParameterList>
als einem mit einer<ConstantScan><Values><Row>
Liste zu erstellen .<ConstantScan><Values><Row>
anstelle von zu verwenden<ParameterList>
.Es ist nicht allzu überraschend: Der Ausführungsplan für die winzige Einfügung wird einmal berechnet und dann 1000 Mal wiederverwendet. Das Parsen und Vorbereiten des Plans ist schnell, da nur vier Werte gelöscht werden müssen. Ein 1000-Zeilen-Plan muss dagegen 4000 Werte (oder 4000 Parameter, wenn Sie Ihre C # -Tests parametrisiert haben) verarbeiten. Dies kann leicht die Zeitersparnis verschlingen, die Sie durch das Eliminieren von 999 Hin- und Rückflügen zu SQL Server erzielen, insbesondere wenn Ihr Netzwerk nicht zu langsam ist.
quelle
Das Problem hängt wahrscheinlich mit der Zeit zusammen, die zum Kompilieren der Abfrage benötigt wird.
Wenn Sie die Einfügungen beschleunigen möchten, müssen Sie sie wirklich in eine Transaktion einschließen:
Ab C # können Sie auch einen Tabellenwertparameter verwenden. Das Ausgeben mehrerer Befehle in einem Stapel durch Trennen durch Semikolons ist ein weiterer Ansatz, der ebenfalls hilfreich ist.
quelle
Ich bin auf eine ähnliche Situation gestoßen, als ich versucht habe, eine Tabelle mit mehreren 100.000 Zeilen mit einem C ++ - Programm (MFC / ODBC) zu konvertieren.
Da dieser Vorgang sehr lange dauerte, habe ich mir vorgenommen, mehrere Einfügungen zu einer zu bündeln (bis zu 1000 aufgrund von MSSQL-Einschränkungen ). Ich vermute, dass viele einzelne Einfügeanweisungen einen ähnlichen Overhead verursachen würden wie hier beschrieben .
Es stellt sich jedoch heraus, dass die Konvertierung tatsächlich etwas länger gedauert hat:
1000 einzelne Aufrufe von CDatabase :: ExecuteSql mit jeweils einer einzelnen INSERT-Anweisung (Methode 1) sind also ungefähr doppelt so schnell wie ein einzelner Aufruf von CDatabase :: ExecuteSql mit einer mehrzeiligen INSERT-Anweisung mit 1000 Wertetupeln (Methode 2).
Update: Als nächstes habe ich versucht, 1000 separate INSERT-Anweisungen in einer einzigen Zeichenfolge zu bündeln und vom Server ausführen zu lassen (Methode 3). Es stellt sich heraus, dass dies sogar etwas schneller ist als Methode 1.
Bearbeiten: Ich verwende Microsoft SQL Server Express Edition (64-Bit) v10.0.2531.0
quelle