web-dev-qa-db-fra.com

Plusieurs instructions INSERT vs un seul INSERT avec plusieurs valeurs

J'effectue une comparaison de performances entre l'utilisation de 1000 instructions 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 utilisant une seule instruction INSERT avec 1000 valeurs:

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)

À ma grande surprise, les résultats sont à l'opposé de ce que je pensais:

  • 1000 instructions INSERT: 290 msec.
  • 1 instruction INSERT avec 1000 VALUES: 2800 msec.

Le test est exécuté directement dans MSSQL Management Studio avec SQL Server Profiler utilisé pour les mesures (et des résultats similaires sont obtenus à partir de code C # avec SqlClient, ce qui est encore plus étonnant compte tenu de tous les allers-retours des couches DAL).

Cela peut-il être raisonnable ou expliqué d'une manière ou d'une autre? Comment se fait-il qu'une méthode soi-disant plus rapide donne 10 fois (!) pire performance?

Je vous remercie.

EDIT: Attacher des plans d'exécution pour les deux: Exec Plans

116
Borka

Ajout: SQL Server 2012 présente de meilleures performances dans ce domaine mais ne semble pas résoudre les problèmes spécifiques décrits ci-dessous. Cela devrait apparemment corrigé dans la prochaine version majeure après SQL Server 2012!

Votre plan indique que les insertions individuelles utilisent des procédures paramétrées (éventuellement paramétrées automatiquement), de sorte que le temps d’analyse/compilation doit être minimal.

Je pensais que je devrais examiner cela un peu plus, donc installer une boucle ( script ) et essayer d'ajuster le nombre de clauses VALUES et d'enregistrer le temps de compilation.

J'ai ensuite divisé le temps de compilation par le nombre de lignes pour obtenir le temps de compilation moyen par clause. Les résultats sont ci-dessous

Graph

Jusqu'à 250 clauses VALUES présentent le temps de compilation/nombre de clauses a une légère tendance à la hausse mais rien de trop dramatique.

Graph

Mais alors il y a un changement soudain.

Cette section des données est indiquée ci-dessous.

+------+----------------+-------------+---------------+---------------+
| 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 taille du plan en cache, qui augmentait de manière linéaire, a soudainement diminué, mais CompileTime a été multiplié par 7 et CompileMemory a augmenté. Il s’agit du point de jonction entre le plan, paramétré automatiquement (avec 1 000 paramètres) et non paramétré. Par la suite, il semble devenir linéairement moins efficace (en termes de nombre de clauses de valeur traitées dans un temps donné).

Je ne sais pas pourquoi cela devrait être. Vraisemblablement, lorsqu’il élabore un plan pour des valeurs littérales spécifiques, il doit exécuter une activité qui n’est pas mise à l’échelle de manière linéaire (comme le tri).

Cela ne semble pas affecter la taille du plan de requête en cache lorsque j'ai essayé une requête entièrement composée de lignes en double, ni l’ordre de la sortie de la table des constantes (et lors de l’insertion dans un tas de temps passé à trier de toute façon, même s’il le faisait).

De plus, si un index en cluster est ajouté à la table, le plan affiche toujours une étape de tri explicite, de sorte qu'il ne semble pas trier au moment de la compilation pour éviter un tri au moment de l'exécution.

Plan

J'ai essayé d'examiner cela dans un débogueur, mais les symboles publics de ma version de SQL Server 2008 ne semblaient pas disponibles, j'ai donc dû examiner la construction équivalente UNION ALL Dans SQL Server 2005.

Une trace de pile typique est ci-dessous

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   

Donc, en supprimant les noms dans la trace de la pile, il semble passer beaucoup de temps à comparer des chaînes.

Cet article de la Base de connaissances indique que DeriveNormalizedGroupProperties est associé à ce que l'on appelait auparavant l'étape de normalisation du traitement de la requête.

Cette étape est maintenant appelée liaison ou algébrization. Elle prend la sortie de l'arbre d'analyse syntaxique d'expression de l'étape précédente et génère une arborescence d'expression algébriquée (arborescence de processeur de requête) pour passer à l'optimisation (optimisation de plan trivial dans ce cas) [ ref] .

J'ai essayé une autre expérience ( Script ) qui consistait à réexécuter le test original mais en examinant trois cas différents.

  1. Prénom et Nom Chaînes d'une longueur de 10 caractères sans doublons.
  2. Prénom et Nom Chaînes d'une longueur de 50 caractères sans doublons.
  3. Prénom et Nom Chaînes de 10 caractères avec tous les doublons.

Graph

On voit clairement que plus les cordes sont longues, plus les choses empirent et inversement, plus il y a de doublons, plus les choses s'améliorent. Comme mentionné précédemment, les doublons n'affectant pas la taille du plan mis en cache, je suppose donc qu'il doit exister un processus d'identification des doublons lors de la construction de l'arborescence des expressions algébriquées.

Modifier

Un endroit où cette information est exploitée est indiqué par @Lieven ici

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

Parce qu’au moment de la compilation, il peut déterminer que la colonne Name n’a pas de doublons; il saute l’ordre lors de l’exécution de l’expression secondaire 1/ (ID - ID) au moment de l’exécution (le tri dans le plan n’a qu’un ORDER BY) et aucune erreur de division par zéro n'est générée. Si des doublons sont ajoutés à la table, l'opérateur de tri affiche deux colonnes par ordre et l'erreur attendue est générée.

121
Martin Smith

Ce n’est pas surprenant: le plan d’exécution du petit insert est calculé une fois, puis réutilisé 1 000 fois. L'analyse et la préparation du plan sont rapides car il ne comporte que quatre valeurs. Par contre, un plan à 1 000 lignes doit traiter 4 000 valeurs (ou 4 000 paramètres si vous avez paramétré vos tests C #). Cela pourrait facilement réduire les gains de temps que vous gagnez en éliminant 999 allers-retours vers SQL Server, en particulier si votre réseau n'est pas excessivement lent.

22
dasblinkenlight

Le problème a probablement à voir avec le temps qu'il faut pour compiler la requête.

Si vous souhaitez accélérer les insertions, ce que vous devez vraiment faire est de les envelopper dans une transaction:

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;

À partir de C #, vous pouvez également envisager d'utiliser un paramètre de valeur table. Émettre plusieurs commandes dans un même lot, en les séparant par des points-virgules, est une autre approche qui aidera également.

9
RickNZ

J'ai rencontré une situation similaire en essayant de convertir une table avec plusieurs lignes 100k avec un programme C++ (MFC/ODBC).

Comme cette opération a pris beaucoup de temps, j'ai imaginé de regrouper plusieurs inserts en un (jusqu'à 1 000 en raison de limitations de MSSQL ). Je suppose que beaucoup d’instructions à insertion unique créeraient une surcharge semblable à ce qui est décrit ici .

Cependant, il s’avère que la conversion a pris un peu plus de temps:

        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

Ainsi, 1 000 appels uniques à CDatabase :: ExecuteSql, chacun avec une seule instruction INSERT (méthode 1), sont environ deux fois plus rapides qu'un seul appel à CDatabase :: ExecuteSql avec une instruction INSERT à plusieurs lignes avec 1000 tuples de valeur (méthode 2).

Mise à jour: J'ai donc essayé ensuite de regrouper 1 000 instructions INSERT distinctes dans une seule chaîne et de laisser le serveur l'exécuter (méthode 3). Il s'avère que c'est même un peu plus rapide que la méthode 1.

Edit: J'utilise Microsoft SQL Server Express Edition (64 bits) v10.0.2531.0

1
uceumern