Dans SQL Server 2016 CTP 2.1, un nouvel objet est apparu après CTP 2.0 :sys.dm_exec_function_stats. Ceci est destiné à fournir des fonctionnalités similaires à sys.dm_exec_procedure_stats, sys.dm_exec_query_stats et sys.dm_exec_trigger_stats. Il est donc désormais possible de suivre les métriques d'exécution agrégées pour les fonctions définies par l'utilisateur.
Ou est-ce ?
Dans CTP 2.1 au moins, je ne pouvais dériver ici que des métriques significatives pour les fonctions scalaires régulières - rien n'était enregistré pour les TVF en ligne ou multi-instructions. Je ne suis pas surpris des fonctions en ligne, car elles sont de toute façon essentiellement développées avant l'exécution. Mais comme les TVF multi-instructions sont souvent des problèmes de performances, j'espérais qu'ils apparaîtraient aussi. Ils apparaissent toujours dans sys.dm_exec_query_stats, vous pouvez donc toujours en dériver leurs mesures de performances, mais il peut être difficile d'effectuer des agrégations lorsque vous avez vraiment plusieurs instructions qui effectuent une partie du travail - rien n'est cumulé pour vous.
Voyons rapidement comment cela se passe. Supposons que nous ayons un tableau simple avec 100 000 lignes :
SELECT TOP (100000) o1.[object_id], o1.create_date INTO dbo.src FROM sys.all_objects AS o1 CROSS JOIN sys.all_objects AS o2 ORDER BY o1.[object_id]; GO CREATE CLUSTERED INDEX x ON dbo.src([object_id]); GO -- prime the cache SELECT [object_id], create_date FROM dbo.src;
Je voulais comparer ce qui se passe lorsque nous étudions les UDF scalaires, les fonctions table à plusieurs instructions et les fonctions table inline, et comment nous voyons le travail effectué dans chaque cas. Tout d'abord, imaginez quelque chose de trivial que nous pouvons faire dans le SELECT
clause, mais que nous voudrions peut-être compartimenter, comme formater une date sous forme de chaîne :
CREATE PROCEDURE dbo.p_dt_Standard @dt_ CHAR(10) = NULL AS BEGIN SET NOCOUNT ON; SELECT @dt_ = CONVERT(CHAR(10), create_date, 120) FROM dbo.src ORDER BY [object_id]; END GO
(J'attribue la sortie à une variable, ce qui force l'analyse de la table entière, mais empêche les mesures de performances d'être influencées par les efforts de SSMS pour consommer et rendre la sortie. Merci pour le rappel, Mikael Eriksson.)
Souvent, vous verrez des gens mettre cette conversion dans une fonction, et cela peut être scalaire ou TVF, comme ceux-ci :
CREATE FUNCTION dbo.dt_Inline(@dt_ DATETIME) RETURNS TABLE AS RETURN (SELECT dt_ = CONVERT(CHAR(10), @dt_, 120)); GO CREATE FUNCTION dbo.dt_Multi(@dt_ DATETIME) RETURNS @t TABLE(dt_ CHAR(10)) AS BEGIN INSERT @t(dt_) SELECT CONVERT(CHAR(10), @dt_, 120); RETURN; END GO CREATE FUNCTION dbo.dt_Scalar(@dt_ DATETIME) RETURNS CHAR(10) AS BEGIN RETURN (SELECT CONVERT(CHAR(10), @dt_, 120)); END GO
J'ai créé des wrappers de procédure autour de ces fonctions comme suit :
CREATE PROCEDURE dbo.p_dt_Inline @dt_ CHAR(10) = NULL AS BEGIN SET NOCOUNT ON; SELECT @dt_ = dt.dt_ FROM dbo.src AS o CROSS APPLY dbo.dt_Inline(o.create_date) AS dt ORDER BY o.[object_id]; END GO CREATE PROCEDURE dbo.p_dt_Multi @dt_ CHAR(10) = NULL AS BEGIN SET NOCOUNT ON; SELECT @dt_ = dt.dt_ FROM dbo.src CROSS APPLY dbo.dt_Multi(create_date) AS dt ORDER BY [object_id]; END GO CREATE PROCEDURE dbo.p_dt_Scalar @dt_ CHAR(10) = NULL AS BEGIN SET NOCOUNT ON; SELECT @dt_ = dt = dbo.dt_Scalar(create_date) FROM dbo.src ORDER BY [object_id]; END GO
(Et non, le dt_
la convention que vous voyez n'est pas quelque chose de nouveau, je pense que c'est une bonne idée, c'était juste la façon la plus simple d'isoler toutes ces requêtes dans les DMV de tout le reste collecté. Il a également facilité l'ajout de suffixes pour distinguer facilement la requête à l'intérieur de la procédure stockée et la version ad hoc.)
Ensuite, j'ai créé une table #temp pour stocker les minutages et répété ce processus (à la fois en exécutant la procédure stockée deux fois et en exécutant deux fois le corps de la procédure en tant que requête ad hoc isolée, et en suivant le minutage de chacun):
CREATE TABLE #t ( ID INT IDENTITY(1,1), q VARCHAR(32), s DATETIME2, e DATETIME2 ); GO INSERT #t(q,s) VALUES('p Standard',SYSDATETIME()); GO EXEC dbo.p_dt_Standard; GO 2 UPDATE #t SET e = SYSDATETIME() WHERE ID = 1; GO INSERT #t(q,s) VALUES('ad hoc Standard',SYSDATETIME()); GO DECLARE @dt_st CHAR(10); SELECT @dt_st = CONVERT(CHAR(10), create_date, 120) FROM dbo.src ORDER BY [object_id]; GO 2 UPDATE #t SET e = SYSDATETIME() WHERE ID = 2; GO -- repeat for inline, multi and scalar versions
Ensuite, j'ai exécuté quelques requêtes de diagnostic, et voici les résultats :
sys.dm_exec_function_stats
SELECT name = OBJECT_NAME(object_id), execution_count, time_milliseconds = total_elapsed_time/1000 FROM sys.dm_exec_function_stats WHERE database_id = DB_ID() ORDER BY name;
Résultats :
name execution_count time_milliseconds --------- --------------- ----------------- dt_Scalar 400000 1116
Ce n'est pas une faute de frappe; seul l'UDF scalaire montre une présence dans le nouveau DMV.
sys.dm_exec_procedure_stats
SELECT name = OBJECT_NAME(object_id), execution_count, time_milliseconds = total_elapsed_time/1000 FROM sys.dm_exec_procedure_stats WHERE database_id = DB_ID() ORDER BY name;
Résultats :
name execution_count time_milliseconds ------------- --------------- ----------------- p_dt_Inline 2 74 p_dt_Multi 2 269 p_dt_Scalar 2 1063 p_dt_Standard 2 75
Ce n'est pas un résultat surprenant :l'utilisation d'une fonction scalaire entraîne une pénalité de performance d'un ordre de grandeur, alors que la TVF multi-instructions n'était qu'environ 4 fois pire. Sur plusieurs tests, la fonction en ligne était toujours aussi rapide ou une milliseconde ou deux plus rapide qu'aucune fonction du tout.
sys.dm_exec_query_stats
SELECT query = SUBSTRING([text],s,e), execution_count, time_milliseconds FROM ( SELECT t.[text], s = s.statement_start_offset/2 + 1, e = COALESCE(NULLIF(s.statement_end_offset,-1),8000)/2, s.execution_count, time_milliseconds = s.total_elapsed_time/1000 FROM sys.dm_exec_query_stats AS s OUTER APPLY sys.dm_exec_sql_text(s.[sql_handle]) AS t WHERE t.[text] LIKE N'%dt[_]%' ) AS x;
Résultats tronqués, réorganisés manuellement :
query (truncated) execution_count time_milliseconds -------------------------------------------------------------------- --------------- ----------------- -- p Standard: SELECT @dt_ = CONVERT(CHAR(10), create_date, 120) ... 2 75 -- ad hoc Standard: SELECT @dt_st = CONVERT(CHAR(10), create_date, 120) ... 2 72 -- p Inline: SELECT @dt_ = dt.dt_ FROM dbo.src AS o CROSS APPLY dbo.dt_Inline... 2 74 -- ad hoc Inline: SELECT @dt_in = dt.dt_ FROM dbo.src AS o CROSS APPLY dbo.dt_Inline... 2 72 -- all Multi: INSERT @t(dt_) SELECT CONVERT(CHAR(10), @dt_, 120); 184 5 -- p Multi: SELECT @dt_ = dt.dt_ FROM dbo.src CROSS APPLY dbo.dt_Multi... 2 270 -- ad hoc Multi: SELECT @dt_m = dt.dt_ FROM dbo.src AS o CROSS APPLY dbo.dt_Multi... 2 257 -- all scalar: RETURN (SELECT CONVERT(CHAR(10), @dt_, 120)); 400000 581 -- p Scalar: SELECT @dt_ = dbo.dt_Scalar(create_date)... 2 986 -- ad hoc Scalar: SELECT @dt_sc = dbo.dt_Scalar(create_date)... 2 902
Une chose importante à noter ici est que le temps en millisecondes pour l'INSERT dans le TVF multi-instructions et l'instruction RETURN dans la fonction scalaire sont également pris en compte dans les SELECT individuels, il n'est donc pas logique d'additionner tout de les horaires.
Chronométrages manuels
Et puis enfin, les timings de la table #temp :
SELECT query = q, time_milliseconds = DATEDIFF(millisecond, s, e) FROM #t ORDER BY ID;
Résultats :
query time_milliseconds --------------- ----------------- p Standard 107 ad hoc Standard 78 p Inline 80 ad hoc Inline 78 p Multi 351 ad hoc Multi 263 p Scalar 992 ad hoc Scalar 907
D'autres résultats intéressants ici :l'encapsuleur de procédure a toujours eu des frais généraux, bien que leur importance puisse être vraiment subjective.
Résumé
Mon propos ici aujourd'hui était simplement de montrer le nouveau DMV en action et de définir correctement les attentes - certaines mesures de performance pour les fonctions seront toujours trompeuses, et certaines ne seront toujours pas disponibles du tout (ou du moins seront très fastidieuses à reconstituer par vous-même ).
Je pense que ce nouveau DMV couvre l'un des plus gros éléments de surveillance des requêtes qui manquait auparavant à SQL Server :les fonctions scalaires sont parfois des tueurs de performances invisibles, car le seul moyen fiable d'identifier leur utilisation était d'analyser le texte de la requête, ce qui est loin d'être infaillible. Peu importe le fait que cela ne vous permettra pas d'isoler leur impact sur les performances, ou que vous auriez dû savoir que vous recherchiez des UDF scalaires dans le texte de la requête en premier lieu.
Annexe
J'ai joint le script :DMExecFunctionStats.zip
De plus, à partir de CTP1, voici l'ensemble des colonnes :
database_id | object_id | type | type_desc | |
sql_handle | plan_handle | cached_time | last_execution_time | execution_count |
total_worker_time | last_worker_time | min_worker_time | max_worker_time | |
total_physical_reads | last_physical_reads | min_physical_reads | max_physical_reads | |
total_logical_writes | last_logical_writes | min_logical_writes | max_logical_writes | |
total_logical_reads | last_logical_reads | min_logical_reads | max_logical_reads | |
total_elapsed_time | last_elapsed_time | min_elapsed_time | max_elapsed_time |
Colonnes actuellement dans sys.dm_exec_function_stats