web-dev-qa-db-fra.com

Performances de bcp / BULK INSERT par rapport aux paramètres de table

Je suis sur le point de devoir réécrire du code assez ancien à l'aide de BULK INSERT commande parce que le schéma a changé, et il m'est venu à l'esprit que je devrais peut-être penser à passer à une procédure stockée avec un TVP à la place, mais je me demande quel effet cela pourrait avoir sur les performances.

Quelques informations générales qui pourraient aider à expliquer pourquoi je pose cette question:

  • Les données arrivent en fait via un service Web. Le service Web écrit un fichier texte dans un dossier partagé sur le serveur de base de données qui effectue à son tour une BULK INSERT. Ce processus a été implémenté à l'origine sur SQL Server 2000 et, à l'époque, il n'y avait vraiment pas d'autre alternative que de supprimer quelques centaines d'instructions INSERT sur le serveur, ce qui était en fait le processus d'origine et un problème de performances.

  • Les données sont insérées en bloc dans une table de transfert permanente, puis fusionnées dans une table beaucoup plus grande (après quoi elles sont supprimées de la table de transfert).

  • La quantité de données à insérer est "grande", mais pas "énorme" - généralement quelques centaines de lignes, peut-être 5 à 10 000 lignes dans de rares cas. Par conséquent, mon instinct est que BULK INSERT le fait d'être une opération non enregistrée ne fera pas que une grande différence (mais bien sûr, je ne suis pas sûr, d'où la question).

  • L'insertion fait en fait partie d'un processus de traitement par lots beaucoup plus important et doit se produire plusieurs fois de suite; donc la performance est critique.

Les raisons pour lesquelles je voudrais remplacer le BULK INSERT avec un TVP sont:

  • Écrire le fichier texte sur NetBIOS coûte probablement déjà un certain temps, et c'est assez horrible d'un point de vue architectural.

  • Je pense que la table intermédiaire peut (et devrait) être supprimée. La raison principale est que les données insérées doivent être utilisées pour quelques autres mises à jour en même temps que l'insertion, et il est beaucoup plus coûteux d'essayer la mise à jour à partir de la table de production massive que d'utiliser une mise en scène presque vide table. Avec un TVP, le paramètre essentiellement est la table de transfert, je peux faire tout ce que je veux avec/avant l'insertion principale.

  • Je pourrais à peu près éliminer la vérification de dupe, le code de nettoyage et tous les frais généraux associés aux insertions en masse.

  • Pas besoin de s'inquiéter de la contention des verrous sur la table intermédiaire ou tempdb si le serveur obtient quelques-unes de ces transactions à la fois (nous essayons de l'éviter, mais cela arrive).

Je vais évidemment décrire cela avant de mettre quoi que ce soit en production, mais j'ai pensé que ce serait une bonne idée de demander d'abord avant de passer tout ce temps, voir si quelqu'un a des avertissements sévères à émettre concernant l'utilisation des TVP à cette fin.

Donc - pour quiconque est suffisamment à l'aise avec SQL Server 2008 pour avoir essayé ou au moins enquêté sur cela, quel est le verdict? Pour les insertions de, disons, de quelques centaines à quelques milliers de rangées, qui se produisent assez fréquemment, les TVP coupent-ils la moutarde? Existe-t-il une différence de performances significative par rapport aux inserts en vrac?


Mise à jour: maintenant avec 92% de points d'interrogation en moins!

(AKA: Résultats des tests)

Le résultat final est maintenant en production après ce qui ressemble à un processus de déploiement en 36 étapes. Les deux solutions ont été largement testées:

  • Extraire le code du dossier partagé et utiliser directement la classe SqlBulkCopy;
  • Passage à une procédure stockée avec TVP.

Afin que les lecteurs puissent se faire une idée de ce qui a été testé exactement, pour dissiper tout doute quant à la fiabilité de ces données, voici une explication plus détaillée de ce que ce processus d'importation fait réellement :

  1. Commencez avec une séquence de données temporelles qui est généralement d'environ 20 à 50 points de données (bien qu'elle puisse parfois atteindre quelques centaines);

  2. Faites-en un tas de traitements fous qui sont pour la plupart indépendants de la base de données. Ce processus est parallélisé, donc environ 8 à 10 des séquences de (1) sont traitées en même temps. Chaque processus parallèle génère 3 séquences supplémentaires.

  3. Prenez les 3 séquences et la séquence d'origine et combinez-les en un lot.

  4. Combinez les lots de toutes les 8 à 10 tâches de traitement maintenant terminées en un seul grand super-lot.

  5. Importez-le en utilisant le BULK INSERT stratégie (voir étape suivante) ou stratégie TVP (passez à l'étape 8).

  6. Utilisez la classe SqlBulkCopy pour vider l'intégralité du super-lot dans 4 tables de transfert permanentes.

  7. Exécutez une procédure stockée qui (a) exécute un tas d'étapes d'agrégation sur 2 des tables, y compris plusieurs conditions JOIN, puis (b) effectue une MERGE sur 6 tables de production en utilisant à la fois le données agrégées et non agrégées. (Fini)

    OU

  8. Générez 4 DataTable objets contenant les données à fusionner; 3 d'entre eux contiennent des types CLR qui, malheureusement, ne sont pas correctement pris en charge par les TVP ADO.NET, ils doivent donc être insérés en tant que représentations de chaîne, ce qui nuit un peu aux performances.

  9. Alimentez les TVP à une procédure stockée, qui effectue essentiellement le même traitement que (7), mais directement avec les tables reçues. (Fini)

Les résultats étaient raisonnablement proches, mais l'approche TVP a finalement donné de meilleurs résultats en moyenne, même lorsque les données dépassaient légèrement 1 000 lignes.

Notez que ce processus d'importation est exécuté plusieurs milliers de fois de suite, il était donc très facile d'obtenir un temps moyen simplement en comptant le nombre d'heures (oui, des heures) qu'il a fallu pour terminer toutes les fusions.

À l'origine, une fusion moyenne prenait presque exactement 8 secondes (sous une charge normale). La suppression du kludge NetBIOS et le passage à SqlBulkCopy ont réduit le temps à presque exactement 7 secondes. Le passage aux TVP a encore réduit le temps à 5,2 secondes par lot. C'est une amélioration de 35% de débit pour un processus dont le temps d'exécution est mesuré en heures - donc pas mal du tout. C'est également une amélioration de ~ 25% par rapport à SqlBulkCopy.

Je suis en fait assez confiant que la véritable amélioration était bien plus que cela. Au cours des tests, il est devenu évident que la fusion finale n'était plus le chemin critique; au lieu de cela, le service Web qui effectuait tout le traitement des données commençait à boucler sous le nombre de demandes entrant. Ni le processeur ni les E/S de la base de données n'étaient vraiment au maximum, et il n'y avait pas d'activité de verrouillage significative. Dans certains cas, nous avons constaté un écart de quelques secondes au ralenti entre les fusions successives. Il y avait un léger écart, mais beaucoup plus petit (une demi-seconde environ) lors de l'utilisation de SqlBulkCopy. Mais je suppose que cela deviendra un conte pour un autre jour.

Conclusion: Les paramètres de valeur de table fonctionnent vraiment mieux que BULK INSERT opérations pour les processus d'importation et de transformation complexes opérant sur des ensembles de données de taille moyenne.


Je voudrais ajouter un autre point, juste pour apaiser toute appréhension de la part des gens qui sont des pro-staging-tables. D'une certaine manière, l'ensemble de ce service est un processus de mise en scène géant. Chaque étape du processus est fortement auditée, nous n'avons donc pas besoin d'une table intermédiaire pour déterminer pourquoi une fusion particulière a échoué (bien qu'en pratique, elle ne arrive). Tout ce que nous avons à faire est de définir un indicateur de débogage dans le service et celui-ci passera au débogueur ou videra ses données dans un fichier au lieu de la base de données.

En d'autres termes, nous avons déjà plus que suffisamment d'informations sur le processus et n'avons pas besoin de la sécurité d'une table intermédiaire; la seule raison pour laquelle nous avions la table de transfert en premier lieu était d'éviter de se débattre sur toutes les instructions INSERT et UPDATE que nous aurions dû utiliser autrement. Dans le processus d'origine, les données de transfert ne vivaient de toute façon dans la table de transfert que pendant des fractions de seconde, de sorte qu'elles n'apportaient aucune valeur en termes de maintenance/maintenabilité.

Notez également que nous avons pas remplacé chaque simple BULK INSERT fonctionnement avec TVP. Plusieurs opérations qui traitent de plus grandes quantités de données et/ou n'ont pas besoin de faire quelque chose de spécial avec les données autres que de les jeter sur la base de données utilisent toujours SqlBulkCopy. Je ne suggère pas que les TVP sont une panacée de performance, mais seulement qu'ils ont réussi sur SqlBulkCopy dans ce cas spécifique impliquant plusieurs transformations entre la mise en scène initiale et la fusion finale.

Alors voilà. Le point revient à TToni pour trouver le lien le plus pertinent, mais j'apprécie également les autres réponses. Merci encore!

75
Aaronaught

Je n'ai pas encore vraiment d'expérience avec TVP, mais il y a un joli tableau de comparaison des performances par rapport à BULK INSERT dans MSDN ici .

Ils disent que BULK INSERT a un coût de démarrage plus élevé, mais est plus rapide par la suite. Dans un scénario de client distant, ils tracent la ligne à environ 1000 lignes (pour une logique de serveur "simple"). À en juger par leur description, je dirais que vous devriez bien utiliser les TVP. Le rendement atteint - le cas échéant - est probablement négligeable et les avantages architecturaux semblent très bons.

Modifier: Sur une note latérale, vous pouvez éviter le fichier local du serveur et toujours utiliser la copie en bloc en utilisant l'objet SqlBulkCopy. Remplissez simplement un DataTable et introduisez-le dans la méthode "WriteToServer" d'une instance SqlBulkCopy. Facile à utiliser et très rapide.

9
TToni

Le tableau mentionné en ce qui concerne le lien fourni dans la réponse de @ TToni doit être replacé dans son contexte. Je ne sais pas combien de recherches réelles ont été consacrées à ces recommandations (notez également que le graphique ne semble être disponible que dans les versions 2008 Et 2008 R2 De cette documentation).

D'autre part, il y a ce livre blanc de l'équipe consultative client SQL Server: Maximizing Throughput with TVP

J'utilise des TVP depuis 2009 et j'ai découvert, du moins d'après mon expérience, que pour tout autre chose qu'une simple insertion dans une table de destination sans besoin de logique supplémentaire (ce qui est rarement le cas), les TVP sont généralement la meilleure option.

J'ai tendance à éviter les tables intermédiaires, car la validation des données doit être effectuée au niveau de la couche d'application. En utilisant des TVP, cela est facilement pris en charge et la variable de table TVP dans la procédure stockée est, par sa nature même, une table de staging localisée (donc pas de conflit avec d'autres processus s'exécutant en même temps que vous obtenez lorsque vous utilisez une vraie table pour le staging ).

En ce qui concerne les tests effectués dans la question, je pense qu'il pourrait être démontré qu'il est encore plus rapide que ce qui a été trouvé à l'origine:

  1. Vous ne devez pas utiliser un DataTable, sauf si votre application l'utilise en dehors de l'envoi des valeurs au TVP. L'utilisation de l'interface IEnumerable<SqlDataRecord> Est plus rapide et utilise moins de mémoire car vous ne dupliquez pas la collection en mémoire uniquement pour l'envoyer à la base de données. Je l'ai documenté dans les endroits suivants:
  2. Les TVP sont des variables de table et, à ce titre, ne conservent pas de statistiques. Cela signifie qu'ils signalent n'avoir qu'une ligne dans l'Optimiseur de requête. Donc, dans votre proc, soit:
    • Utilisez la recompilation au niveau de l'instruction sur toutes les requêtes utilisant le TVP pour autre chose qu'un simple SELECT: OPTION (RECOMPILE)
    • Créez une table temporaire locale (c.-à-d. Unique #) Et copiez le contenu du TVP dans la table temporaire
6
Solomon Rutzky

Je pense que je resterais avec une approche d'insertion en vrac. Vous pouvez constater que tempdb est toujours atteint en utilisant un TVP avec un nombre raisonnable de lignes. C'est mon intuition, je ne peux pas dire que j'ai testé les performances de l'utilisation de TVP (je suis également intéressé à entendre les commentaires des autres)

Vous ne mentionnez pas si vous utilisez .NET, mais l'approche que j'ai adoptée pour optimiser les solutions précédentes était de faire un chargement en masse de données en utilisant la classe SqlBulkCopy - vous n'avez pas besoin d'écrire les données dans un fichier avant le chargement, il suffit de donner à la classe SqlBulkCopy (par exemple) un DataTable - c'est le moyen le plus rapide pour insérer des données dans la base de données. 5 à 10 000 lignes, ce n'est pas grand-chose, je l'ai utilisé pour jusqu'à 750 000 lignes. Je soupçonne qu'en général, avec quelques centaines de lignes, cela ne ferait pas une grande différence en utilisant un TVP. Mais l'extension serait limitée à mon humble avis.

Peut-être que la nouvelle fonctionnalité FUSIONNER dans SQL 2008 vous serait bénéfique?

De plus, si votre table de transfert existante est une table unique utilisée pour chaque instance de ce processus et que vous êtes préoccupé par les conflits, etc., avez-vous envisagé de créer une nouvelle table de transfert "temporaire" mais physique à chaque fois, puis de la supprimer lorsqu'elle est fini avec?

Notez que vous pouvez optimiser le chargement dans cette table intermédiaire, en la remplissant sans index. Ensuite, une fois rempli, ajoutez tous les index requis à ce stade (FILLFACTOR = 100 pour des performances de lecture optimales, car à ce stade, il ne sera pas mis à jour).

4
AdaTheDev

Les tables de mise en scène sont bonnes! Vraiment, je ne voudrais pas le faire autrement. Pourquoi? Parce que les importations de données peuvent changer de façon inattendue (et souvent d'une manière que vous ne pouvez pas prévoir, comme le temps où les colonnes étaient encore appelées prénom et nom mais avaient les données de prénom dans la colonne nom, par exemple, pour choisir un exemple non au hasard.) Facile à rechercher le problème avec une table intermédiaire afin que vous puissiez voir exactement quelles données se trouvaient dans les colonnes que l'importation a traitées. Plus difficile à trouver, je pense, lorsque vous utilisez une table en mémoire. Je connais beaucoup de gens qui font des importations pour gagner ma vie et tous recommandent d'utiliser des tables de transfert. Je soupçonne qu'il y a une raison à cela.

Corriger davantage un petit changement de schéma dans un processus de travail est plus facile et prend moins de temps que de repenser le processus. Si cela fonctionne et que personne n'est prêt à payer des heures pour le changer, ne corrigez que ce qui doit être corrigé en raison du changement de schéma. En modifiant l'ensemble du processus, vous introduisez beaucoup plus de nouveaux bogues potentiels qu'en apportant une petite modification à un processus de travail existant et testé.

Et comment allez-vous supprimer toutes les tâches de nettoyage des données? Vous les faites peut-être différemment, mais elles doivent encore être faites. Encore une fois, changer le processus comme vous le décrivez est très risqué.

Personnellement, il me semble que vous êtes simplement offensé en utilisant des techniques plus anciennes plutôt qu'en ayant la chance de jouer avec de nouveaux jouets. Vous semblez n'avoir aucune base réelle pour vouloir changer autre que l'insertion en vrac est donc 2000.

0
HLGEM