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