Comment mesurer ou trouver le coût de création d'un plan de requête?

18

J'ai un cas typique où le reniflage de paramètres fait atterrir un «mauvais» plan d'exécution dans le cache du plan, ce qui rend les exécutions ultérieures de ma procédure stockée très lentes. Je peux "résoudre" ce problème avec des variables locales,, OPTIMIZE FOR ... UNKNOWNet OPTION(RECOMPILE). Cependant, je peux également plonger dans la requête et essayer de l'optimiser.

J'essaie de déterminer si je devrais : étant donné le temps limité pour résoudre les problèmes, je voudrais savoir le coût de ne pas le faire. À mon avis, si je continue OPTION(RECOMPILE), l'effet net est qu'un plan de requête est recréé chaque fois que la requête est exécutée. Donc, je pense que je dois savoir:

Comment connaître le coût de création d' un plan de requête?

Pour répondre à ma propre question, j'ai googlé (par exemple avec cette requête ), et j'ai parcouru la documentation des colonnes pour le dm_exec_query_statsDMV . J'ai également inspecté la fenêtre de sortie dans SSMS pour "Plan de requête réel" pour trouver ces informations. Enfin, je l' ai cherché DBA.SE . Aucun de ceux-ci n'a conduit à une réponse.

Quelqu'un peut-il me le dire? Est-il possible de trouver ou de mesurer le temps nécessaire à la création d'un plan?

Jeroen
la source
5
Je recommanderais de récupérer une copie de Inside the SQL Server Query Optimizer de Benjamin Nevarez . C'est gratuit. Le chapitre 5 «Le processus d'optimisation» peut vous aider à déterminer le temps de compilation de votre requête. À tout le moins, il est instructif sur ce que l'optimiseur passe par pour créer un plan de requête.
Mark Sinkinson

Réponses:

18

Comment connaître le coût de création d'un plan de requête?

Vous pouvez consulter les propriétés du nœud racine dans le plan de requête, par exemple:

Extrait des propriétés des racines
(capture d'écran de l' explorateur gratuit Sentry One Plan )

Ces informations sont également disponibles en interrogeant le cache du plan, par exemple à l'aide d'une requête basée sur les relations suivantes:

WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT 
    CompileTime = c.value('(QueryPlan/@CompileTime)[1]', 'int'),
    CompileCPU = c.value('(QueryPlan/@CompileCPU)[1]', 'int'),
    CompileMemory = c.value('(QueryPlan/@CompileMemory)[1]', 'int'),
    ST.[text],
    QP.query_plan
FROM sys.dm_exec_cached_plans AS CP
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) AS QP
CROSS APPLY sys.dm_exec_sql_text(CP.plan_handle) AS ST
CROSS APPLY QP.query_plan.nodes('ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS N(c);

Fragment des résultats

Pour un traitement complet des options dont vous disposez pour gérer ce type de requêtes, consultez l'article récemment mis à jour d' Erland Sommarskog .

Paul White réintègre Monica
la source
4

En supposant que le "coût" est en termes de temps (bien que vous ne sachiez pas quoi d'autre il pourrait être en termes de ;-), alors vous devriez au moins avoir une idée de cela en faisant quelque chose comme ceci:

DBCC FREEPROCCACHE WITH NO_INFOMSGS;

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases'; -- replace with your proc

SET STATISTICS TIME OFF;

Le premier élément signalé dans l'onglet "Messages" doit être:

Temps d'analyse et de compilation SQL Server:

Je l'exécuterais au moins 10 fois et ferais la moyenne des millisecondes "CPU" et "Elapsed".

Dans l'idéal, vous l'exécuteriez dans Production afin d'obtenir une estimation de l'heure réelle, mais les utilisateurs sont rarement autorisés à vider le cache du plan dans Production. Heureusement, à partir de SQL Server 2008, il est devenu possible d'effacer un plan spécifique du cache. Dans ce cas, vous pouvez effectuer les opérations suivantes:

DECLARE @SQL NVARCHAR(MAX) = '';
;WITH cte AS
(
  SELECT DISTINCT stat.plan_handle
  FROM sys.dm_exec_query_stats stat
  CROSS APPLY sys.dm_exec_text_query_plan(stat.plan_handle, 0, -1) qplan
  WHERE qplan.query_plan LIKE N'%sp[_]help%' -- replace "sp[_]help" with proc name
)
SELECT @SQL += N'DBCC FREEPROCCACHE ('
               + CONVERT(NVARCHAR(130), cte.plan_handle, 1)
               + N');'
               + NCHAR(13) + NCHAR(10)
FROM cte;
PRINT @SQL;
EXEC (@SQL);

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases' -- replace with your proc

SET STATISTICS TIME OFF;

Cependant, en fonction de la variabilité des valeurs transmises pour le ou les paramètres à l'origine du "mauvais" plan mis en cache, il existe une autre méthode à considérer qui est un juste milieu entre OPTION(RECOMPILE)et OPTION(OPTIMIZE FOR UNKNOWN): Dynamic SQL. Oui, je l'ai dit. Et je veux dire même Dynamic SQL non paramétré. Voici pourquoi.

Vous avez clairement des données qui ont une distribution inégale, au moins en termes d'une ou plusieurs valeurs de paramètres d'entrée. Les inconvénients des options mentionnées sont:

  • OPTION(RECOMPILE)générera un plan pour chaque exécution et vous ne pourrez jamais bénéficier d'une quelconque réutilisation du plan, même si les valeurs des paramètres retransmises sont identiques aux exécutions précédentes. Pour les procs qui sont appelés fréquemment - une fois toutes les quelques secondes ou plus fréquemment - cela vous évitera une situation horrible occasionnelle, mais vous laissera toujours dans une situation toujours pas si géniale.

  • OPTION(OPTIMIZE FOR (@Param = value)) générera un plan basé sur cette valeur particulière, ce qui pourrait aider plusieurs cas, mais vous laissera toujours ouvert au problème actuel.

  • OPTION(OPTIMIZE FOR UNKNOWN)générera un plan basé sur ce qui équivaut à une distribution moyenne, ce qui aidera certaines requêtes mais en blessera d'autres. Cela devrait être le même que l'option d'utiliser des variables locales.

Le SQL dynamique, cependant, une fois correctement effectué , permettra aux différentes valeurs transmises d'avoir leurs propres plans de requête séparés qui sont idéaux (enfin, autant qu'ils vont l'être). Le coût principal ici est que, à mesure que la variété des valeurs transmises augmente, le nombre de plans d'exécution dans le cache augmente et ils prennent de la mémoire. Les coûts mineurs sont:

  • besoin de valider les paramètres de chaîne pour empêcher les injections SQL

  • éventuellement besoin de configurer un certificat et un utilisateur basé sur un certificat pour maintenir une abstraction de sécurité idéale, car Dynamic SQL nécessite des autorisations de table directes.

Donc, voici comment j'ai géré cette situation lorsque j'ai eu des procs qui ont été appelés plus d'une fois par seconde et qui ont touché plusieurs tables, chacune avec des millions de lignes. J'avais essayé OPTION(RECOMPILE)mais cela s'est avéré beaucoup trop préjudiciable au processus dans les 99% des cas qui n'avaient pas le problème de reniflage de paramètre / problème de plan mis en cache. Et n'oubliez pas que l'un de ces procs contenait environ 15 requêtes et seulement 3 à 5 d'entre elles ont été converties en Dynamic SQL comme décrit ici; Le SQL dynamique n'a été utilisé que s'il était nécessaire pour une requête particulière.

  1. S'il existe plusieurs paramètres d'entrée dans la procédure stockée, déterminez ceux qui sont utilisés avec des colonnes qui ont des distributions de données très disparates (et par conséquent provoquent ce problème) et ceux qui sont utilisés avec des colonnes qui ont des distributions plus régulières (et ne devraient pas être à l'origine de ce problème).

  2. Générez la chaîne Dynamic SQL à l'aide de paramètres pour les paramètres d'entrée proc associés à des colonnes réparties uniformément. Ce paramétrage permet de réduire l'augmentation résultante des plans d'exécution dans le cache liée à cette requête.

  3. Pour les autres paramètres associés à des distributions très variées, ceux-ci doivent être concaténés dans Dynamic SQL sous forme de valeurs littérales. Puisqu'une requête unique est déterminée par toute modification du texte de la requête, avoir WHERE StatusID = 1est une requête différente, et donc, un plan de requête différent, qu'avoir WHERE StatusID = 2.

  4. Si l'un des paramètres d'entrée de proc qui doivent être concaténés dans le texte de la requête sont des chaînes, ils doivent être validés pour se protéger contre l'injection SQL (bien que cela soit moins susceptible de se produire si les chaînes transmises sont générées par le application et pas un utilisateur, mais quand même). Faites au moins le REPLACE(@Param, '''', '''''')pour vous assurer que les guillemets simples deviennent des guillemets simples échappés.

  5. Si nécessaire, créez un certificat qui sera utilisé pour créer un utilisateur et signez la procédure stockée de sorte que les autorisations de table directes ne seront accordées qu'au nouvel utilisateur basé sur un certificat et non [public]aux utilisateurs qui ne devraient pas autrement avoir de telles autorisations. .

Exemple de proc:

CREATE PROCEDURE MySchema.MyProc
(
  @Param1 INT,
  @Param2 DATETIME,
  @Param3 NVARCHAR(50)
)
AS
SET NOCOUNT ON;

DECLARE @SQL NVARCHAR(MAX);

SET @SQL = N'
     SELECT  tab.Field1, tab.Field2, ...
     FROM    MySchema.SomeTable tab
     WHERE   tab.Field3 = @P1
     AND     tab.Field8 >= CONVERT(DATETIME, ''' +
  CONVERT(NVARCHAR(50), @Param2, 121) +
  N''')
     AND     tab.Field2 LIKE N''' +
  REPLACE(@Param3, N'''', N'''''') +
  N'%'';';

EXEC sp_executesql
     @SQL,
     N'@P1 INT',
     @P1 = @Param1;
Solomon Rutzky
la source
Merci d'avoir pris (pas mal de temps) pour répondre! Je suis un peu sceptique quant au premier bit sur la façon d'obtenir le temps de compilation, étant donné que c'est un facteur 3 inférieur au résultat que j'obtiens en utilisant l'approche de @ PaulWhite . - Le second sur le bit Dynamic SQL est intéressant (même s'il nécessiterait également du temps pour l'implémenter; au moins plus que de simplement gifler un OPTIONsur ma requête), et ne me ferait pas trop de mal car ce sproc est bien exploité dans les tests d'intégration. - Dans tous les cas: merci pour vos idées!
Jeroen