it-swarm.com.de

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

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 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, wenn ich ihn aus C # -Code mit SqlClient ausführe, was in Anbetracht aller DAL-Layer-Roundtrips noch überraschender ist).

Kann das vernünftig oder irgendwie erklärt werden? Wie kommt es, dass eine vermeintlich schnellere Methode zu einer 10-fachen (!) schlechteren Leistung führt?

Vielen Dank.

BEARBEITEN: Ausführungspläne für beide anhängen: Exec Plans

116
Borka

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

Ihr Plan zeigt, dass die einzelnen Inserts 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 eine Schleife einrichten ( script ) und versuchen, die Anzahl der VALUES -Klauseln anzupassen und die Kompilierungszeit aufzuzeichnen.

Ich habe dann die Kompilierzeit durch die Anzahl der Zeilen geteilt, um die durchschnittliche Kompilierzeit pro Klausel zu erhalten. Die Ergebnisse sind unten

Graph

Bis zu 250 VALUES -Klauseln zeigen die Kompilierungszeit/Anzahl der Klauseln einen leichten Aufwärtstrend, aber nichts allzu 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, der linear gewachsen war, sinkt plötzlich, aber CompileTime erhöht sich um das Siebenfache 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 Werteklauseln).

Ich bin mir nicht sicher, warum das so sein sollte. Vermutlich muss beim Kompilieren eines Plans für bestimmte Literalwerte eine Aktivität ausgeführt werden, die sich nicht linear skalieren lässt (z. B. Sortieren).

Es scheint keinen Einfluss auf die Größe des zwischengespeicherten Abfrageplans zu haben, als ich eine vollständig aus doppelten Zeilen bestehende Abfrage ausprobierte, und es wirkt sich auch nicht auf die Reihenfolge der Ausgabe der Konstantentabelle aus (und da Sie in einen Heap Zeit für das Sortieren aufwenden) wäre sowieso sinnlos, auch wenn es so wäre).

Wenn der Tabelle ein Clustered-Index hinzugefügt wird, wird im Plan weiterhin ein expliziter Sortierschritt angezeigt, sodass beim Kompilieren keine Sortierung erfolgt, um eine Sortierung zur Laufzeit zu vermeiden.

Plan

Ich habe versucht, dies in einem Debugger zu untersuchen, aber die öffentlichen Symbole für meine Version von SQL Server 2008 scheinen nicht verfügbar zu sein. Daher musste ich mir die entsprechende UNION ALL - Konstruktion in SQL Server 2005 ansehen.

Eine typische Stapelverfolgung finden Sie weiter 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 DeriveNormalizedGroupProperties der so genannten Normalisierungs- Phase der Abfrageverarbeitung zugeordnet ist

Diese Stufe wird jetzt als Binden oder Algebrisieren bezeichnet. Sie verwendet den Ausdrucksanalysebaum, der aus der vorherigen Analysestufe ausgegeben wurde, und gibt einen algebrisierten Ausdrucksbaum (Abfrageprozessorbaum) aus, um mit der Optimierung fortzufahren (in diesem Fall Optimierung mit einfachen Plänen) [ ref] .

Ich habe noch ein Experiment versucht ( Script ), bei dem der ursprüngliche Test wiederholt werden sollte, 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 sind, 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 der Duplikatidentifizierung durchgeführt werden muss.

Bearbeiten

Ein Ort, an dem diese Informationen genutzt werden, ist hier von @Lieven gezeigt

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

Da beim Kompilieren festgestellt werden kann, dass die Spalte Name keine Duplikate enthält, wird die Reihenfolge nach dem sekundären Ausdruck 1/ (ID - ID) zur Laufzeit übersprungen (die Sortierung im Plan hat nur einen ORDER BY Spalte) und es wird kein Fehler durch Null dividiert. Wenn der Tabelle Duplikate hinzugefügt werden, zeigt der Sortieroperator zwei Spaltenreihenfolgen an und der erwartete Fehler wird ausgelöst.

121
Martin Smith

Kein Wunder: Der Ausführungsplan für den winzigen Einsatz wird einmal berechnet und dann 1000-mal wiederverwendet. Das Parsen und Vorbereiten des Plans ist schnell, da nur vier Werte zum Löschen zur Verfügung stehen. Ein 1000-Zeilen-Plan muss dagegen 4000 Werte verarbeiten (oder 4000 Parameter, wenn Sie Ihre C # -Tests parametrisiert haben). Dies kann die Zeitersparnis, die Sie durch das Eliminieren von 999 Roundtrips zu SQL Server erzielen, aufzehren, insbesondere wenn Ihr Netzwerk nicht zu langsam ist.

22
dasblinkenlight

Das Problem hat wahrscheinlich mit der Zeit zu tun, die zum Kompilieren der Abfrage benötigt wird.

Wenn Sie die Einfügungen beschleunigen möchten, müssen Sie sie in eine Transaktion einwickeln:

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 Tabellenwert-Parameter verwenden. Ein weiterer Ansatz, der ebenfalls hilfreich ist, besteht darin, mehrere Befehle in einem Stapel durch Semikolon zu trennen.

9
RickNZ

In einer ähnlichen Situation versuchte ich, eine Tabelle mit mehreren 100-KB-Zeilen mit einem C++ - Programm (MFC/ODBC) zu konvertieren.

Da dieser Vorgang sehr lange dauerte, dachte ich, dass mehrere Einfügungen in einer gebündelt werden sollten (bis zu 1000 aufgrund von MSSQL-Einschränkungen ). Ich vermute, dass viele einzelne Einfügeanweisungen einen ähnlichen Overhead verursachen würden wie hier beschrieben hier .

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

Somit sind 1000 Einzelaufrufe von CDatabase :: ExecuteSql mit jeweils einer INSERT-Anweisung (Methode 1) ungefähr doppelt so schnell wie ein Einzelaufruf 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 den 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) 10.0.2531.0

1
uceumern