Instructions INSERT multiples et INSERT unique avec plusieurs valeurs

119

J'exécute 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 en 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 VALEURS: 2800 msec.

Le test est exécuté directement dans MSSQL Management Studio avec SQL Server Profiler utilisé pour la mesure (et j'ai des résultats similaires en l'exécutant à partir de code C # à l'aide de SqlClient, ce qui est encore plus surprenant compte tenu de toutes 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 supposément plus rapide entraîne des performances 10 fois (!) Moins bonnes?

Je vous remercie.

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

Borka
la source
1
ce sont des tests propres, rien ne s'exécute en parallèle, pas de données répétées (chaque requête est avec des données différentes, bien sûr, pour éviter une simple mise en cache)
Borka
1
y a-t-il des déclencheurs impliqués?
AK
2
J'ai converti un programme en TVP pour dépasser la limite de 1000 valeurs et j'ai obtenu un gain de performances important. Je vais faire une comparaison.
paparazzo

Réponses:

126

Ajout: SQL Server 2012 montre des performances améliorées dans ce domaine mais ne semble pas résoudre les problèmes spécifiques mentionnés ci-dessous. Cela devrait apparemment être corrigé dans la prochaine version majeure après SQL Server 2012!

Votre plan montre que les insertions simples utilisent des procédures paramétrées (éventuellement auto-paramétrées), donc le temps d'analyse / compilation pour ceux-ci doit être minimal.

J'ai pensé que j'examinerais un peu plus cela, alors configurez une boucle ( script ) et essayez d'ajuster le nombre de VALUESclauses 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

Graphique

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

Graphique

Mais ensuite, il y a un changement soudain.

Cette section des données est présenté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 mis en cache, qui avait augmenté de manière linéaire, diminue soudainement, mais CompileTime augmente de 7 fois et CompileMemory augmente. C'est le point de coupure entre un plan paramétré automatiquement (avec 1000 paramètres) et un plan 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 compile un plan pour des valeurs littérales spécifiques, il doit effectuer une activité qui ne se met pas à l'échelle de manière linéaire (comme le tri).

Cela ne semble pas affecter la taille du plan de requête mis en cache lorsque j'ai essayé une requête composée entièrement de lignes en double et ni l'un ni l'autre n'affecte l'ordre de sortie de la table des constantes (et que vous insérez dans un tas de temps passé à trier serait de toute façon inutile même si c'était le cas).

De plus, si un index clusterisé est ajouté à la table, le plan affiche toujours une étape de tri explicite afin 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é de regarder cela dans un débogueur mais les symboles publics de ma version de SQL Server 2008 ne semblent pas être disponibles, alors j'ai dû regarder la UNION ALLconstruction équivalente 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 les chaînes.

Cet article de la base deDeriveNormalizedGroupProperties connaissances indique qu'il est associé à ce que l'on appelait auparavant l' étape de normalisation du traitement des requêtes

Cette étape est maintenant appelée liaison ou algébrizing et elle prend la sortie de l'arbre d'analyse d'expression de l'étape d'analyse précédente et produit un arbre d'expression algébriqué (arbre du processeur de requête) pour aller de l'avant à 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 d'origine mais en examinant trois cas différents.

  1. Prénom et nom Chaînes de 10 caractères sans doublons.
  2. Prénom et nom Chaînes de 50 caractères sans doublon.
  3. Prénom et nom Chaînes de 10 caractères avec tous les doublons.

Graphique

On peut clairement voir que plus les chaînes sont longues, plus les choses se détériorent et qu'à l'inverse, plus il y a de doublons, meilleures sont les choses. Comme mentionné précédemment, les doublons n'affectent pas la taille du plan mis en cache, donc je suppose qu'il doit y avoir un processus d'identification des doublons lors de la construction de l'arbre d'expression algébriqué lui-même.

Éditer

Un endroit où ces informations sont exploitées est montré 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 Namecolonne n'a pas de doublons, il ignore 1/ (ID - ID)le tri par expression secondaire au moment de l'exécution (le tri dans le plan n'a qu'une seule ORDER BYcolonne) et aucune erreur de division par zéro n'est déclenchée. Si des doublons sont ajoutés à la table, l'opérateur de tri affiche deux ordre par colonnes et l'erreur attendue est générée.

Martin Smith
la source
6
Le nombre magique que vous avez est NumberOfRows / ColumnCount = 250. Modifiez votre requête pour n'utiliser que trois colonnes et le changement se produira à 333. Le nombre magique 1000 pourrait être quelque chose comme le nombre maximum de paramètres utilisés dans un plan mis en cache. Il semble être "plus facile" de générer un plan avec un plan <ParameterList>avec une <ConstantScan><Values><Row>liste.
Mikael Eriksson
1
@MikaelEriksson - D'accord. La ligne 250 avec 1000 valeurs est automatiquement paramétrée, la ligne 251 ne le fait pas, donc cela semble être la différence. Je ne sais pas pourquoi. Peut-être qu'il passe du temps à trier les valeurs littérales à la recherche de doublons ou quelque chose quand il en a.
Martin Smith
1
C'est un problème assez fou, je viens d'être chagriné par cela. C'est une excellente réponse merci
Pas aimé
1
@MikaelEriksson Voulez-vous dire que le nombre magique est NumberOfRows * ColumnCount = 1000?
paparazzo
1
@Blam - Oui. Lorsque le nombre total d'éléments est supérieur à 1000 (NumberOfRows * ColumnCount), le plan de requête a changé pour utiliser à la <ConstantScan><Values><Row>place de <ParameterList>.
Mikael Eriksson
23

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

dasblinkenlight
la source
9

Le problème est probablement lié au temps nécessaire 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 table. L'émission de plusieurs commandes dans un seul lot, en les séparant par des points-virgules, est une autre approche qui aidera également.

RickNZ
la source
1
Re: "Émettre plusieurs commandes en un seul lot": cela aide un peu, mais pas beaucoup. Mais je suis tout à fait d'accord avec les deux autres options, soit envelopper dans une TRANSACTION (est-ce que TRANS fonctionne réellement ou devrait-il être simplement TRAN?) Ou utiliser un TVP.
Solomon Rutzky
1

Je suis tombé sur une situation similaire en essayant de convertir une table avec plusieurs lignes de 100 000 avec un programme C ++ (MFC / ODBC).

Comme cette opération a pris très longtemps, j'ai pensé regrouper plusieurs inserts en un seul (jusqu'à 1000 en raison des limitations de MSSQL ). Je suppose que beaucoup d'instructions d'insertion unique créeraient une surcharge similaire à ce qui est décrit ici .

Cependant, il s'avère que la conversion a pris en fait 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, 1000 appels simples à 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 multiligne avec 1000 tuples de valeur (méthode 2).

Mise à jour: Donc, la prochaine chose que j'ai essayée était de regrouper 1000 instructions INSERT séparées dans une seule chaîne et de faire exécuter par le serveur (méthode 3). Il s'avère que c'est encore un peu plus rapide que la méthode 1.

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

uceumern
la source