Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

SQL Server 2016 :sys.dm_exec_function_stats

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