Più istruzioni INSERT rispetto a un singolo INSERT con più VALORI


119

Sto eseguendo un confronto delle prestazioni tra l'utilizzo di 1000 istruzioni INSERT:

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 utilizzando una singola istruzione INSERT con 1000 valori:

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)

Con mia grande sorpresa, i risultati sono l'opposto di quello che pensavo:

  • 1000 istruzioni INSERT: 290 msec.
  • 1 istruzione INSERT con 1000 VALUES: 2800 msec.

Il test viene eseguito direttamente in MSSQL Management Studio con SQL Server Profiler utilizzato per la misurazione (e ho ottenuto risultati simili eseguendolo da codice C # utilizzando SqlClient, il che è ancora più sorprendente considerando tutti i livelli DAL roundtrip)

Può essere ragionevole o in qualche modo spiegato? Come mai un metodo apparentemente più veloce produce prestazioni 10 volte (!) Peggiori ?

Grazie.

EDIT: allegare piani di esecuzione per entrambi: Piani esecutivi


1
questi sono test puliti, niente viene eseguito in parallelo, nessun dato ripetuto (ogni query è con dati diversi, ovviamente, per evitare una semplice memorizzazione nella cache)
Borka

1
ci sono trigger coinvolti?
AK

2
Ho convertito un programma in TVP per superare il limite di 1000 valori e ottenere un grande miglioramento delle prestazioni. Farò un confronto.
paparazzo

Risposte:


126

Aggiunta: SQL Server 2012 mostra alcune prestazioni migliorate in quest'area ma non sembra affrontare i problemi specifici indicati di seguito. Apparentemente questo dovrebbe essere risolto nella prossima versione principale dopo SQL Server 2012!

Il tuo piano mostra che i singoli inserti utilizzano procedure parametrizzate (possibilmente parametrizzate automaticamente), quindi il tempo di analisi / compilazione per questi dovrebbe essere minimo.

Ho pensato di esaminarlo un po 'di più, quindi ho impostato un ciclo ( script ) e ho provato a regolare il numero di VALUESclausole e registrare il tempo di compilazione.

Ho quindi diviso il tempo di compilazione per il numero di righe per ottenere il tempo di compilazione medio per clausola. I risultati sono di seguito

Grafico

Fino a 250 VALUESclausole presenti il ​​tempo di compilazione / numero di clausole ha una leggera tendenza al rialzo ma niente di troppo drammatico.

Grafico

Ma poi c'è un cambiamento improvviso.

Quella sezione dei dati è mostrata di seguito.

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

La dimensione del piano memorizzato nella cache che era cresciuta in modo lineare diminuisce improvvisamente, ma CompileTime aumenta di 7 volte e CompileMemory spara. Questo è il punto di separazione tra il piano che è un piano auto parametrizzato (con 1.000 parametri) e uno non parametrizzato. Successivamente sembra diventare linearmente meno efficiente (in termini di numero di clausole di valore elaborate in un dato tempo).

Non sono sicuro del motivo per cui dovrebbe essere. Presumibilmente, quando compila un piano per valori letterali specifici, deve eseguire alcune attività che non vengono ridimensionate in modo lineare (come l'ordinamento).

Non sembra influenzare la dimensione del piano di query memorizzato nella cache quando ho provato una query composta interamente da righe duplicate e né influisce sull'ordine dell'output della tabella delle costanti (e mentre stai inserendo in un heap il tempo impiegato per l'ordinamento sarebbe comunque inutile anche se lo facesse).

Inoltre, se un indice cluster viene aggiunto alla tabella, il piano mostra ancora un passaggio di ordinamento esplicito, quindi non sembra essere l'ordinamento in fase di compilazione per evitare un ordinamento in fase di esecuzione.

Piano

Ho provato a guardarlo in un debugger ma i simboli pubblici per la mia versione di SQL Server 2008 non sembrano essere disponibili, quindi ho dovuto guardare la UNION ALLcostruzione equivalente in SQL Server 2005.

Di seguito è riportata una tipica traccia dello stack

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   

Quindi, escludendo i nomi nella traccia dello stack, sembra che passi molto tempo a confrontare le stringhe.

Questo articolo della Knowledge Base indica che DeriveNormalizedGroupPropertiesè associato a quella che veniva chiamata la fase di normalizzazione dell'elaborazione delle query

Questa fase è ora chiamata associazione o algebrizzazione e prende l'output dell'albero di analisi dell'espressione dalla fase di analisi precedente e genera un albero delle espressioni algebrizzato (albero del processore di query) per passare all'ottimizzazione (ottimizzazione del piano banale in questo caso) [ref] .

Ho provato un altro esperimento ( Script ) che era di rieseguire il test originale ma esaminando tre casi diversi.

  1. Stringhe di nome e cognome di 10 caratteri senza duplicati.
  2. Stringhe di nome e cognome di 50 caratteri senza duplicati.
  3. Stringhe di nome e cognome di 10 caratteri con tutti i duplicati.

Grafico

Si può chiaramente vedere che più lunghe sono le stringhe, peggiori sono le cose e che viceversa più duplicati si ottengono le cose migliori. Come accennato in precedenza, i duplicati non influiscono sulla dimensione del piano memorizzato nella cache, quindi presumo che ci debba essere un processo di identificazione dei duplicati durante la costruzione dell'albero delle espressioni algebrizzato stesso.

modificare

Un punto in cui queste informazioni vengono sfruttate è mostrato da @Lieven qui

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

Poiché in fase di compilazione può determinare che la Namecolonna non ha duplicati, salta l'ordinamento in base 1/ (ID - ID)all'espressione secondaria in fase di esecuzione (l'ordinamento nel piano ha solo una ORDER BYcolonna) e non viene generato alcun errore di divisione per zero. Se vengono aggiunti duplicati alla tabella, l'operatore di ordinamento mostra due ordini per colonne e viene generato l'errore previsto.


6
Il numero magico che hai è NumberOfRows / ColumnCount = 250. Cambia la tua query per usare solo tre colonne e la modifica avverrà a 333. Il numero magico 1000 potrebbe essere qualcosa come il numero massimo di parametri usati in un piano memorizzato nella cache. Sembra essere "più facile" generare un piano con uno <ParameterList>di uno con una <ConstantScan><Values><Row>lista.
Mikael Eriksson

1
@MikaelEriksson - Concordato. La riga 250 con 1000 valori viene parametrizzata automaticamente, la riga 251 no, quindi questa sembra essere la differenza. Non sono sicuro del perché però. Forse passa il tempo a ordinare i valori letterali alla ricerca di duplicati o qualcosa del genere quando li ha.
Martin Smith,

1
Questo è un problema piuttosto folle, ne sono appena stato addolorato. Questa è un'ottima risposta, grazie
Non amato il

1
@MikaelEriksson Vuoi dire che il numero magico è NumberOfRows * ColumnCount = 1000?
paparazzo

1
@Blam - Sì. Quando il numero totale di elementi è superiore a 1000 (NumberOfRows * ColumnCount), il piano di query è stato modificato per utilizzare al <ConstantScan><Values><Row>posto di <ParameterList>.
Mikael Eriksson

23

Non è troppo sorprendente: il piano di esecuzione per il minuscolo inserto viene calcolato una volta e quindi riutilizzato 1000 volte. L'analisi e la preparazione del piano è veloce, perché ha solo quattro valori da eliminare. Un piano a 1000 righe, d'altra parte, deve gestire 4000 valori (o 4000 parametri se hai parametrizzato i tuoi test C #). Ciò potrebbe facilmente consumare il risparmio di tempo ottenuto eliminando 999 viaggi di andata e ritorno a SQL Server, soprattutto se la rete non è eccessivamente lenta.


9

Il problema probabilmente ha a che fare con il tempo necessario per compilare la query.

Se vuoi velocizzare gli inserimenti, quello che devi veramente fare è avvolgerli in una transazione:

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;

Da C #, potresti anche considerare l'utilizzo di un parametro con valori di tabella. L'emissione di più comandi in un unico batch, separandoli con punto e virgola, è un altro approccio che aiuterà anche.


1
Ri: "Emissione di più comandi in un unico batch": questo aiuta poco, ma non molto. Ma sono decisamente d'accordo con le altre due opzioni di avvolgere in una TRANSAZIONE (TRANS funziona effettivamente o dovrebbe essere solo TRAN?) O utilizzare un TVP.
Solomon Rutzky

1

Mi sono imbattuto in una situazione simile cercando di convertire una tabella con diverse 100k righe con un programma C ++ (MFC / ODBC).

Poiché questa operazione richiedeva molto tempo, ho pensato di raggruppare più inserti in uno (fino a 1000 a causa delle limitazioni di MSSQL ). La mia ipotesi è che molte singole istruzioni di inserimento creerebbero un sovraccarico simile a quello descritto qui .

Tuttavia, risulta che la conversione in realtà ha richiesto un po 'più di tempo:

        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

Quindi, 1000 singole chiamate a CDatabase :: ExecuteSql ciascuna con una singola istruzione INSERT (metodo 1) sono circa due volte più veloci di una singola chiamata a CDatabase :: ExecuteSql con un'istruzione INSERT multilinea con 1000 tuple di valori (metodo 2).

Aggiornamento: Quindi, la prossima cosa che ho provato è stata di raggruppare 1000 istruzioni INSERT separate in una singola stringa e fare in modo che il server lo eseguisse (metodo 3). Si scopre che questo è anche un po 'più veloce del metodo 1.

Modifica: sto utilizzando Microsoft SQL Server Express Edition (64 bit) v10.0.2531.0

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.