Mehrere INSERT-Anweisungen im Vergleich zu einem einzelnen INSERT mit mehreren VALUES


119

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: Pläne ausführen


1
Dies sind saubere Tests, nichts wird parallel ausgeführt, keine wiederholten Daten (jede Abfrage enthält natürlich unterschiedliche Daten, um ein einfaches Caching zu vermeiden)
Borka

1
Gibt es irgendwelche Auslöser?
AK

2
Ich habe ein Programm auf TVP konvertiert, um die 1000-Werte-Grenze zu überschreiten, und einen großen Leistungsgewinn erzielt. Ich werde einen Vergleich durchführen.
Paparazzo

Antworten:


126

Ergänzung: SQL Server 2012 weist in diesem Bereich eine verbesserte Leistung auf, scheint jedoch die unten aufgeführten spezifischen Probleme nicht zu lösen. Dies sollte anscheinend in der nächsten Hauptversion nach SQL Server 2012 behoben werden !

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

Graph

Bis zu 250 VALUESKlauseln weisen die Kompilierungszeit / Anzahl der Klauseln einen leichten Aufwärtstrend auf, aber nichts zu dramatisches.

Graph

Aber dann gibt es eine plötzliche Veränderung.

Dieser Abschnitt der Daten wird unten gezeigt.

+------+----------------+-------------+---------------+---------------+
| Rows | CachedPlanSize | CompileTime | CompileMemory | Duration/Rows |
+------+----------------+-------------+---------------+---------------+
|  245 |            528 |          41 |          2400 | 0.167346939   |
|  246 |            528 |          40 |          2416 | 0.162601626   |
|  247 |            528 |          38 |          2416 | 0.153846154   |
|  248 |            528 |          39 |          2432 | 0.157258065   |
|  249 |            528 |          39 |          2432 | 0.156626506   |
|  250 |            528 |          40 |          2448 | 0.16          |
|  251 |            400 |         273 |          3488 | 1.087649402   |
|  252 |            400 |         274 |          3496 | 1.087301587   |
|  253 |            400 |         282 |          3520 | 1.114624506   |
|  254 |            408 |         279 |          3544 | 1.098425197   |
|  255 |            408 |         290 |          3552 | 1.137254902   |
+------+----------------+-------------+---------------+---------------+

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.

Planen

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 ALLKonstruktion in SQL Server 2005 ansehen .

Eine typische Stapelverfolgung finden Sie unten

sqlservr.exe!FastDBCSToUnicode()  + 0xac bytes  
sqlservr.exe!nls_sqlhilo()  + 0x35 bytes    
sqlservr.exe!CXVariant::CmpCompareStr()  + 0x2b bytes   
sqlservr.exe!CXVariantPerformCompare<167,167>::Compare()  + 0x18 bytes  
sqlservr.exe!CXVariant::CmpCompare()  + 0x11f67d bytes  
sqlservr.exe!CConstraintItvl::PcnstrItvlUnion()  + 0xe2 bytes   
sqlservr.exe!CConstraintProp::PcnstrUnion()  + 0x35e bytes  
sqlservr.exe!CLogOp_BaseSetOp::PcnstrDerive()  + 0x11a bytes    
sqlservr.exe!CLogOpArg::PcnstrDeriveHandler()  + 0x18f bytes    
sqlservr.exe!CLogOpArg::DeriveGroupProperties()  + 0xa9 bytes   
sqlservr.exe!COpArg::DeriveNormalizedGroupProperties()  + 0x40 bytes    
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x18a bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!CQuery::PqoBuild()  + 0x3cb bytes  
sqlservr.exe!CStmtQuery::InitQuery()  + 0x167 bytes 
sqlservr.exe!CStmtDML::InitNormal()  + 0xf0 bytes   
sqlservr.exe!CStmtDML::Init()  + 0x1b bytes 
sqlservr.exe!CCompPlan::FCompileStep()  + 0x176 bytes   
sqlservr.exe!CSQLSource::FCompile()  + 0x741 bytes  
sqlservr.exe!CSQLSource::FCompWrapper()  + 0x922be bytes    
sqlservr.exe!CSQLSource::Transform()  + 0x120431 bytes  
sqlservr.exe!CSQLSource::Compile()  + 0x2ff bytes   

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 DeriveNormalizedGroupPropertiesmit der sogenannten Normalisierungsphase der Abfrageverarbeitung verbunden ist

Diese 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.

  1. Vorname und Nachname Zeichenfolgen mit einer Länge von 10 Zeichen ohne Duplikate.
  2. Vorname und Nachname Zeichenfolgen mit einer Länge von 50 Zeichen ohne Duplikate.
  3. Vorname und Nachname Zeichenfolgen mit einer Länge von 10 Zeichen mit allen Duplikaten.

Graph

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

SELECT * 
FROM (VALUES ('Lieven1', 1),
             ('Lieven2', 2),
             ('Lieven3', 3))Test (name, ID)
ORDER BY name, 1/ (ID - ID) 

Da zur Kompilierungszeit festgestellt werden kann, dass die NameSpalte keine Duplikate enthält, wird die Reihenfolge nach dem sekundären 1/ (ID - ID)Ausdruck zur Laufzeit übersprungen (die Sortierung im Plan enthält nur eine ORDER BYSpalte) 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.


6
Die magische Zahl, die Sie haben, ist NumberOfRows / ColumnCount = 250. Ändern Sie Ihre Abfrage so, dass nur drei Spalten verwendet werden, und die Änderung erfolgt bei 333. Die magische Zahl 1000 kann ungefähr der maximalen Anzahl von Parametern entsprechen, die in einem zwischengespeicherten Plan verwendet werden. Es scheint "einfacher" zu sein, einen Plan mit einem <ParameterList>als einem mit einer <ConstantScan><Values><Row>Liste zu erstellen .
Mikael Eriksson

1
@MikaelEriksson - Einverstanden. Die 250er Zeile mit 1000 Werten wird automatisch parametrisiert, die 251er Zeile nicht, so dass dies der Unterschied zu sein scheint. Ich weiß nicht warum. Vielleicht verbringt es Zeit damit, die Literalwerte nach Duplikaten oder etwas anderem zu sortieren, wenn es diese hat.
Martin Smith

1
Dies ist ein ziemlich verrücktes Thema, ich habe mich nur darüber geärgert. Dies ist eine großartige Antwort, danke
Nicht geliebt am

1
@MikaelEriksson Meinst du, die magische Zahl ist NumberOfRows * ColumnCount = 1000?
Paparazzo

1
@Blam - Ja. Wenn die Gesamtzahl der Elemente mehr als 1000 beträgt (NumberOfRows * ColumnCount), wurde der Abfrageplan geändert, um <ConstantScan><Values><Row>anstelle von zu verwenden <ParameterList>.
Mikael Eriksson

23

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.


9

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:

BEGIN TRAN;
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);
COMMIT TRAN;

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.


1
Betreff: "Mehrere Befehle in einem Stapel ausgeben": Das hilft ein wenig, aber nicht viel. Aber ich stimme definitiv den beiden anderen Optionen zu, entweder eine TRANSAKTION einzuschließen (funktioniert TRANS tatsächlich oder sollte es nur TRAN sein?) Oder einen TVP zu verwenden.
Solomon Rutzky

1

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:

        Method 1       Method 2     Method 3 
        Single Insert  Multi Insert Joined Inserts
Rows    1000           1000         1000
Insert  390 ms         765 ms       270 ms
per Row 0.390 ms       0.765 ms     0.27 ms

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.