Paramètre Sniffing vs VARIABLES vs Recompile vs OPTIMISE POUR INCONNU


40

Nous avons donc eu une longue procédure causant des problèmes ce matin (30 secondes et plus). Nous avons décidé de vérifier si le reniflage des paramètres était à blâmer. Nous avons donc réécrit le proc et paramétré les paramètres entrants en variables afin de neutraliser le sniffing des paramètres. Une approche éprouvée / vraie. Bam, temps de requête amélioré (moins de 1 seconde). Lors de l'examen du plan de requête, les améliorations ont été trouvées dans un index que l'original n'utilisait pas.

Juste pour vérifier que nous n’avions pas obtenu de faux positif, nous avons procédé à un dbcc freeproccache sur la procédure initiale et avons effectué une nouvelle analyse pour voir si les résultats améliorés seraient les mêmes. Mais, à notre grande surprise, le processus initial était encore lent. Nous avons réessayé avec WITH RECOMPILE, toujours lent (nous avons tenté une recompilation sur l’appel du proc et à l’intérieur du proc iself). Nous avons même redémarré le serveur (dev box évidemment).

Donc, ma question est la suivante ... comment peut-on blâmer le paramètre sniffing lorsque nous obtenons la même requête lente sur un cache de plan vide ... il ne devrait pas y avoir de paramètre à renifler ???

Sommes-nous plutôt affectés par des statistiques de table non liées au cache de plan? Et si oui, pourquoi définir les paramètres entrants sur des variables aiderait-il ??

Lors de tests supplémentaires, nous avons également constaté que l’insertion de l’OPTION (OPTIMIZE FOR UNKNOWN) sur les éléments internes du processus DID obtenait le plan amélioré attendu.

Alors, certains d'entre vous plus intelligents que moi, pouvez-vous donner des indices sur ce qui se passe dans les coulisses pour produire ce type de résultat?

Sur une autre note, le plan lent est également interrompu tôt avec raison GoodEnoughPlanFoundalors que le plan rapide n'a pas de raison d'abandonner tôt dans le plan réel.

En résumé

  • Création de variables à partir de paramètres entrants (1 sec)
  • avec recompiler (30+ sec)
  • dbcc freeproccache (30+ s)
  • OPTION (OPTIMISER POUR UKNOWN) (1 sec)

MISE À JOUR:

Voir le plan d'exécution lente ici: https://www.dropbox.com/s/cmx2lrsea8q8mr6/plan_slow.xml

Voir le plan d'exécution rapide ici: https://www.dropbox.com/s/b28x6a01w7dxsed/plan_fast.xml

Remarque: les noms de table, de schéma et d'objet ont été modifiés pour des raisons de sécurité.

Réponses:


43

La requête est

SELECT SUM(Amount) AS SummaryTotal
FROM   PDetail WITH(NOLOCK)
WHERE  ClientID = @merchid
       AND PostedDate BETWEEN @datebegin AND @dateend 

La table contient 103 129 000 lignes.

Le plan rapide recherche par ClientId un prédicat résiduel sur la date, mais doit effectuer 96 recherches pour extraire le Amount. La <ParameterList>section dans le plan est la suivante.

        <ParameterList>
          <ColumnReference Column="@dateend" 
                           ParameterRuntimeValue="'2013-02-01 23:59:00.000'" />
          <ColumnReference Column="@datebegin" 
                           ParameterRuntimeValue="'2013-01-01 00:00:00.000'" />
          <ColumnReference Column="@merchid" 
                           ParameterRuntimeValue="(78155)" />
        </ParameterList>

Le plan lent recherche par date et comporte des recherches pour évaluer le prédicat résiduel sur ClientId et extraire le montant (1 estimé par rapport à réel 7 388 383). La <ParameterList>section est

        <ParameterList>
          <ColumnReference Column="@EndDate" 
                           ParameterCompiledValue="'2013-02-01 23:59:00.000'" 
                           ParameterRuntimeValue="'2013-02-01 23:59:00.000'" />
          <ColumnReference Column="@BeginDate" 
                           ParameterCompiledValue="'2013-01-01 00:00:00.000'"               
                           ParameterRuntimeValue="'2013-01-01 00:00:00.000'" />
          <ColumnReference Column="@ClientID" 
                           ParameterCompiledValue="(78155)" 
                           ParameterRuntimeValue="(78155)" />
        </ParameterList>

Dans ce second cas, le ParameterCompiledValuen'est pas vide. SQL Server a correctement détecté les valeurs utilisées dans la requête.

Le livre "Dépannage pratique de SQL Server 2005" parle de l'utilisation de variables locales

Utiliser des variables locales pour éliminer les paramètres de détection est une astuce assez courante, mais les astuces OPTION (RECOMPILE)et OPTION (OPTIMIZE FOR)... sont généralement des solutions plus élégantes et légèrement moins risquées


Remarque

Dans SQL Server 2005, la compilation au niveau des instructions permet de différer la compilation d'une instruction individuelle dans une procédure stockée jusque juste avant la première exécution de la requête. D'ici là, la valeur de la variable locale sera connue. Théoriquement, SQL Server pourrait en tirer parti pour détecter les valeurs de variables locales de la même manière que les paramètres. Toutefois, comme il était courant d’utiliser des variables locales pour neutraliser le contrôle des paramètres dans SQL Server 7.0 et SQL Server 2000+, le contrôle des variables locales n’était pas activé dans SQL Server 2005. Il pourrait être activé dans une version ultérieure de SQL Server bien raison d'utiliser l'une des autres options décrites dans ce chapitre si vous avez le choix.


À partir d'un test rapide, le comportement décrit ci-dessus est toujours le même en 2008 et 2012 et les variables ne sont pas détectées pour la compilation différée, mais uniquement lorsqu'un OPTION RECOMPILEindice explicite est utilisé.

DECLARE @N INT = 0

CREATE TABLE #T ( I INT );

/*Reference to #T means this statement is subject to deferred compile*/
SELECT *
FROM   master..spt_values
WHERE  number = @N
       AND EXISTS(SELECT COUNT(*) FROM #T)

SELECT *
FROM   master..spt_values
WHERE  number = @N
OPTION (RECOMPILE)

DROP TABLE #T 

Malgré la compilation différée, la variable n’est pas détectée et le nombre estimé de lignes est inexact.

Estimations vs réelles

Je suppose donc que le plan lent concerne une version paramétrée de la requête.

La valeur ParameterCompiledValueest égale à ParameterRuntimeValuepour tous les paramètres; il ne s'agit donc pas d'un sniffing de paramètres classique (le plan a été compilé pour un ensemble de valeurs puis exécuté pour un autre ensemble de valeurs).

Le problème est que le plan qui est compilé pour les valeurs de paramètre correctes est inapproprié.

Vous rencontrez probablement des problèmes avec les dates croissantes décrites ici et ici . Pour une table de 100 millions de lignes, vous devez insérer (ou modifier) ​​20 millions de personnes avant que SQL Server ne mette automatiquement à jour les statistiques. Il semble que la dernière fois qu'ils aient été mis à jour, aucune ligne ne correspond à la plage de dates de la requête, mais 7 millions le sont maintenant.

Vous pouvez planifier des mises à jour plus fréquentes des statistiques, envisager des indicateurs de trace 2389 - 90ou les utiliser OPTIMIZE FOR UKNOWNpour éviter les suppositions plutôt que de pouvoir utiliser les statistiques actuellement trompeuses de la datetimecolonne.

Cela pourrait ne pas être nécessaire dans la prochaine version de SQL Server (après 2012). Un élément Connect associé contient la réponse intrigante

Publié par Microsoft le 28/08/2012 à 13h35
Nous avons apporté une amélioration à l'estimation de la cardinalité pour la prochaine version majeure qui résout ce problème. Restez à l'écoute pour plus de détails une fois que nos aperçus seront publiés. Eric

Cette amélioration de 2014 est examinée par Benjamin Nevarez vers la fin de l'article:

Premier aperçu du nouvel estimateur de cardinalité SQL Server .

Il semble que le nouvel estimateur de cardinalité recule et utilise la densité moyenne dans ce cas plutôt que de donner l'estimation à 1 ligne.

Quelques détails supplémentaires sur l'estimateur de cardinalité de 2014 et le problème clé croissant ici:

Nouvelle fonctionnalité dans SQL Server 2014 - Partie 2 - Nouvelle estimation de cardinalité


29

Ma question est donc la suivante: comment peut-on blâmer le paramètre sniffing lorsque nous obtenons la même requête lente sur un cache de plan vide ... il ne devrait pas y avoir de paramètre à renifler?

Lorsque SQL Server compile une requête contenant des valeurs de paramètre, il détecte les valeurs spécifiques de ces paramètres pour l'estimation de cardinalité (nombre de lignes). Dans votre cas, les valeurs particulières de @BeginDate, @EndDateet @ClientIDsont utilisés lors du choix d' un plan d'exécution. Vous pouvez trouver plus de détails sur le paramètre reniflant ici et ici . Je fournis ces liens de fond car la question ci-dessus me fait penser que le concept est actuellement mal compris - il y a toujours des valeurs de paramètre à renifler lorsqu'un plan est élaboré.

Quoi qu'il en soit, c'est tout à fait hors de propos, car le sniffing de paramètres n'est pas le problème ici, comme l'a souligné Martin Smith. Au moment de la compilation de la requête lente, les statistiques indiquaient qu’il n’existait aucune ligne pour les valeurs reniflées de @BeginDateet @EndDate:

Plan lent reniflé des valeurs

Les valeurs reniflées sont très récentes, suggérant le problème clé croissant mentionné par Martin. Comme il est estimé que la recherche d'index sur les dates ne renvoie qu'une seule ligne, l'optimiseur choisit un plan qui transmet le prédicat ClientIDà l'opérateur Key Lookup.

L'estimation sur une seule ligne est également la raison pour laquelle l'optimiseur cesse de rechercher de meilleurs plans et renvoie un message Bon plan suffisant trouvé. Le coût total estimé du plan lent avec l'estimation sur une seule ligne est de 0,013136 unités de coût, il est donc inutile d'essayer de trouver quelque chose de mieux. Sauf que, bien entendu, la recherche renvoie en réalité 7 388 383 lignes au lieu d'une, ce qui entraîne le même nombre de recherches de clé.

Les statistiques peuvent être délicates à garder à jour et utiles sur des tables volumineuses, et le partitionnement pose des problèmes qui lui sont propres à cet égard. Je n’ai pas eu beaucoup de succès avec les indicateurs de suivi 2389 et 2390, mais vous pouvez les tester. Les versions les plus récentes de SQL Server (R2 SP1 et versions ultérieures) disposent de mises à jour de statistiques dynamiques , mais ces mises à jour de statistiques par partition ne sont toujours pas implémentées. En attendant, vous souhaiterez peut-être planifier une mise à jour manuelle des statistiques chaque fois que vous apportez des modifications importantes à ce tableau.

Pour cette requête particulière, je penserais à implémenter l'index suggéré par l'optimiseur lors de la compilation du plan de requête rapide:

/*
The Query Processor estimates that implementing the following index could improve
the query cost by 98.8091%.

WARNING: This is only an estimate, and the Query Processor is making this 
recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide impact,
including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index>]
ON [dbo].[PDetail] ([ClientID],[PostedDate])
INCLUDE ([Amount]);

L'index doit être aligné sur les partitions, avec une ON PartitionSchemeName (PostedDate)clause, mais le fait de fournir un meilleur chemin d'accès aux données aidera l'optimiseur à éviter les mauvais choix de plans, sans recourir à des OPTIMIZE FOR UNKNOWNastuces ni à des solutions de contournement démodées, telles que l'utilisation de variables locales.

Avec l'index amélioré, la recherche de clé pour extraire la Amountcolonne sera éliminée, le processeur de requêtes peut toujours effectuer une élimination dynamique de la partition et utiliser une recherche pour trouver le domaine ClientIDet la plage de dates.


J'aimerais pouvoir marquer deux réponses comme étant correctes, mais encore une fois, merci pour l'info supplémentaire - très instructif.
RThomas

1
Cela fait quelques années que j'ai posté ceci ... mais je voulais juste vous le faire savoir. J'utilise toujours le terme "imparfaitement compris" pendant tout le temps perdu, et je pense toujours à Paul White quand je le fais. Me fait rire à chaque fois.
RThomas

0

J'ai eu exactement le même problème lorsqu'une procédure stockée est devenue lente et OPTIMIZE FOR UNKNOWNque RECOMPILEles conseils de requête ont résolu le problème et accéléré le temps d'exécution. Toutefois, les deux méthodes suivantes n'ont pas affecté la lenteur de la procédure stockée: (i) Effacement du cache (ii) à l'aide de WITH RECOMPILE. Donc, comme vous l'avez dit, ce n'était pas vraiment un reniflement de paramètres.

Les drapeaux de trace 2389 et 2390 n’ont pas aidé non plus. Il suffit de mettre à jour les statistiques ( EXEC sp_updatestats) pour moi.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.