Requête T-SQL utilisant un plan complètement différent en fonction du nombre de lignes que je mets à jour

20

J'ai une instruction SQL UPDATE avec une clause "TOP (X)", et la ligne dans laquelle je mets à jour les valeurs contient environ 4 milliards de lignes. Lorsque j'utilise "TOP (10)", j'obtiens un plan d'exécution qui s'exécute presque instantanément, mais lorsque j'utilise "TOP (50)" ou plus, la requête ne se termine jamais (du moins, pas en attendant), et il utilise un plan d'exécution complètement différent. La requête plus petite utilise un plan très simple avec une paire de recherches d'index et une jointure en boucle imbriquée, où la même requête exacte (avec un nombre différent de lignes dans la clause TOP de l'instruction UPDATE) utilise un plan qui implique deux recherches d'index différentes , une bobine de table, le parallélisme et un tas d'autres complexités.

J'ai utilisé "OPTION (USE PLAN ...)" pour le forcer à utiliser le plan d'exécution généré par la requête plus petite - lorsque je fais cela, je peux mettre à jour jusqu'à 100 000 lignes en quelques secondes. Je sais que le plan de requête est bon, mais SQL Server ne choisira ce plan que lorsqu'il ne s'agit que d'un petit nombre de lignes - tout nombre de lignes décemment élevé dans ma mise à jour se traduira par un plan sous-optimal.

Je pensais que le parallélisme était peut-être à blâmer, alors j'ai mis MAXDOP 1la requête, mais en vain - cette étape a disparu, mais le mauvais choix / performances ne l'est pas. J'ai également couru sp_updatestatsce matin pour m'assurer que ce n'était pas la cause.

J'ai joint les deux plans d'exécution - le plus court est aussi le plus rapide. De plus, voici la requête en question (il convient de noter que le SELECT que j'ai inclus semble être rapide dans les cas de petits et de grands nombres de lignes):

    update top (10000) FactSubscriberUsage3
               set AccountID = sma.CustomerID
    --select top 50 f.AccountID, sma.CustomerID
      from FactSubscriberUsage3 f
      join dimTime t
        on f.TimeID = t.TimeID
      join #mac sma
        on f.macid = sma.macid
       and t.TimeValue between sma.StartDate and sma.enddate 
     where f.AccountID = 0 --There's a filtered index on the table for this

Voici le plan rapide : Plan d'exécution rapide

Et voici le plus lent : Plan d'exécution lente

Y a-t-il quelque chose d'évident dans la façon dont je configure ma requête ou dans le plan d'exécution à condition qu'il se prête au mauvais choix que fait le moteur de requête? Si nécessaire, je peux également inclure les définitions de table impliquées et les index qui y sont définis.

Pour ceux qui ont demandé une version uniquement statistique des objets de la base de données: je ne savais même pas que vous pouviez le faire, mais c'est tout à fait logique! J'ai essayé de générer les scripts pour une base de données uniquement pour que les autres puissent tester les plans d'exécution par eux-mêmes, mais je peux générer des statistiques / histogrammes sur mon index filtré (erreur de syntaxe dans le script, semble-t-il), donc je suis pas de chance là-bas. J'ai essayé de supprimer le filtre et les plans de requête étaient proches, mais pas exactement les mêmes, et je ne veux envoyer personne à une chasse aux oies.

Mise à jour et quelques plans d'exécution plus complets: Tout d'abord, l'Explorateur de plans de SQL Sentry est un outil incroyable. Je ne savais même pas qu'il existait avant de voir les autres questions sur le plan de requête sur ce site, et il avait beaucoup à dire sur la façon dont mes requêtes s'exécutaient. Bien que je ne sois pas sûr de la façon de résoudre le problème, ils ont rendu évident le problème.

Voici le résumé pour 10, 100 et 1000 lignes - vous pouvez voir que la requête de 1000 lignes est très différente des autres: Résumé de la déclaration

Vous pouvez voir que la troisième requête a un nombre ridicule de lectures, donc elle fait évidemment quelque chose de complètement différent. Voici le plan d'exécution estimé, avec le nombre de lignes. Plan d'exécution estimé à 1000 lignes: Plan d'exécution estimé à 1 000 lignes

Et voici les résultats réels du plan d'exécution (en passant, par "ne finit jamais", il s'avère que je voulais dire "finit en une heure"). Plan d'exécution réel à 1 000 lignes Plan d'exécution réel à 1 000 lignes

La première chose que je remarque est que, au lieu de tirer 60K lignes de la table DimTime comme il attend, il est en fait tirer 1,6 milliard, avec un B . En regardant ma requête, je ne sais pas comment il retire autant de lignes de la table dimTime. L'opérateur BETWEEN que j'utilise garantit simplement que je tire le bon enregistrement de #mac en fonction de l'enregistrement de temps dans la table de faits. Cependant, lorsque j'ajoute une ligne à la clause WHERE où je filtre t.TimeValue (ou t.TimeID) sur une seule valeur, je peux mettre à jour 100 000 lignes en quelques secondes. En conséquence, et comme indiqué clairement dans les plans d'exécution que j'ai inclus, il est évident que c'est mon calendrier qui pose problème, mais je ne sais pas comment je changerais les critères de jointure pour contourner ce problème et maintenir la précision . Des pensées?

Pour référence, voici le plan (avec le nombre de lignes) pour la mise à jour de 100 lignes. Vous pouvez voir qu'il atteint le même index, et toujours avec une tonne de lignes, mais loin de la même ampleur d'un problème. Exécution de 100 lignes avec nombre de lignes : entrez la description de l'image ici

SqlRyan
la source
Ce sont des statistiques GOTTA Be. Avez-vous exécuté un sp_updatestatisticssur la table?
JNK
@JNK: Au départ, je le pensais, mais j'ai déjà exécuté sp_updatestats sans changement. Je viens de l'exécuter à nouveau et il ne se souciait pas de mettre à jour les statistiques sur l'un des index impliqués dans la requête. Merci quand même!
SqlRyan
Le second est un plan de mise à jour large (par index) plutôt qu'un plan étroit (par ligne) qui explique une partie de la complexité supplémentaire visible. Mais vraiment, la seule différence est l'ordre de jointure from #mac sma join f on f.macid = sma.macid join dimTime t on f.TimeID = t.TimeID and t.TimeValue between sma.StartDate and sma.enddatevsfrom #mac join t on t.TimeValue between sma.StartDate and sma.enddate join f on f.TimeID = t.TimeID and f.macid = sma.macid
Martin Smith
Quelque chose ne va pas ici. Même le plan de requête coûteux devrait générer des lignes de manière incrémentielle. Un TOP 50devrait toujours s'exécuter rapidement. Pouvez-vous télécharger les plans XML? Je dois regarder le nombre de lignes. Pouvez-vous exécuter le TOP 50avec maxdop 1 et en tant que sélection, pas en tant que mise à jour et publier le plan? (Essayer de simplifier / diviser l'espace de recherche).
usr
@usr se joindre à t.TimeValue between sma.StartDate and sma.enddatepourrait finir par générer beaucoup plus de lignes inutiles qui seront ensuite filtrées dans la jointure contre FactSubscriber et ne se retrouveront donc pas dans le résultat final.
Martin Smith

Réponses:

3

L'index sur dimTime change. Le plan le plus rapide utilise un index _dta. Tout d'abord, assurez-vous qu'il n'est pas marqué comme index hypothétique dans sys.indexes.

En pensant que vous pourriez contourner certains paramètres en utilisant la table #mac pour filtrer au lieu de simplement fournir les dates de début / fin comme celle-ci WHERE t.TimeValue entre @StartDate et @enddate. Débarrassez-vous de cette table temporaire.

william_a_dba
la source
L'index préfixé dta ressemble simplement à sa création en suivant une recommandation DTA sans personnaliser le nom. Les index hypothétiques ne peuvent pas apparaître dans les plans d'exécution réels (et ne seront pas estimés non plus sans certaines commandes non documentées). Je ne sais pas comment votre deuxième suggestion fonctionnerait. t.TimeValue between sma.StartDate and sma.enddateest corrélé et peut donc changer pour chaque ligne du #temptableau. Avec quoi l'OP le remplacerait-il?
Martin Smith
Assez juste, je n'ai pas fait assez attention à la table temporaire.
william_a_dba
1
Cependant, des indices hypothétiques peuvent en effet bousiller un plan d'exécution. Si elle est hypothétique, elle doit être supprimée et recréée. blogs.technet.com/b/anurag_sharma/archive/2008/04/15/…
william_a_dba
Les index hypothétiques sont laissés lorsque le DTA ne se termine pas / se fige avant la fin. Vous devez les nettoyer manuellement s'il y a un hoquet avec le DTA.
william_a_dba
1
@william_a_dba - Ah je vois ce que tu veux dire maintenant (après avoir lu ton lien). La requête ne se termine jamais aurait pu être une recompilation continue. Intéressant!
Martin Smith
1

Sans plus d'informations sur le nombre de lignes dans le plan, ma recommandation préliminaire est d'organiser l'ordre de jointure correct dans la requête et de le forcer à utiliser OPTION (FORCE ORDER). Appliquez l'ordre de jointure du premier plan.

usr
la source