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.
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).
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.
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 = 1
est une requête différente, et donc, un plan de requête différent, qu'avoir WHERE StatusID = 2
.
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.
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;