sql >> Base de Datos >  >> RDS >> Sqlserver

Servidor SQL 2016:sys.dm_exec_function_stats

En SQL Server 2016 CTP 2.1, hay un nuevo objeto que apareció después de CTP 2.0:sys.dm_exec_function_stats. Esto tiene como objetivo proporcionar una funcionalidad similar a sys.dm_exec_procedure_stats, sys.dm_exec_query_stats y sys.dm_exec_trigger_stats. Por lo tanto, ahora es posible rastrear métricas de tiempo de ejecución agregadas para funciones definidas por el usuario.

¿O lo es?

Al menos en CTP 2.1, solo pude derivar métricas significativas aquí para funciones escalares regulares:no se registró nada para TVF en línea o de múltiples declaraciones. No me sorprenden las funciones en línea, ya que de todos modos se expanden esencialmente antes de la ejecución. Pero dado que los TVF de declaraciones múltiples a menudo son problemas de rendimiento, esperaba que también aparecieran. Todavía aparecen en sys.dm_exec_query_stats, por lo que aún puede derivar sus métricas de rendimiento a partir de ahí, pero puede ser complicado realizar agregaciones cuando realmente tiene varias declaraciones que realizan una parte del trabajo:no se acumula nada para usted.

Echemos un vistazo rápido a cómo se desarrolla esto. Digamos que tenemos una tabla simple con 100 000 filas:

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;

Quería comparar lo que sucede cuando investigamos UDF escalares, funciones con valores de tabla de múltiples declaraciones y funciones con valores de tabla en línea, y cómo vemos qué trabajo se realizó en cada caso. Primero, imagina algo trivial que podamos hacer en el SELECT cláusula, pero que tal vez queramos compartimentar, como formatear una fecha como una cadena:

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

(Asigno la salida a una variable, lo que obliga a escanear toda la tabla, pero evita que las métricas de rendimiento se vean influenciadas por los esfuerzos de SSMS para consumir y representar la salida. Gracias por el recordatorio, Mikael Eriksson).

Muchas veces verás personas poniendo esa conversión en una función, y puede ser escalar o TVF, como estas:

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

Creé contenedores de procedimientos alrededor de estas funciones de la siguiente manera:

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

(Y no, el dt_ La convención que está viendo no es algo nuevo, creo que es una buena idea, fue la forma más sencilla en que pude aislar todas estas consultas en los DMV de todo lo demás que se recopila. También facilitó agregar sufijos para distinguir fácilmente entre la consulta dentro del procedimiento almacenado y la versión ad hoc).

A continuación, creé una tabla #temp para almacenar los tiempos y repetí este proceso (tanto ejecutando el procedimiento almacenado dos veces como ejecutando el cuerpo del procedimiento como una consulta ad hoc aislada dos veces y rastreando el tiempo de cada uno):

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

Luego ejecuté algunas consultas de diagnóstico y estos fueron los resultados:

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;

Resultados:

name        execution_count    time_milliseconds
---------   ---------------    -----------------
dt_Scalar   400000             1116

Eso no es un error tipográfico; solo el UDF escalar muestra alguna presencia en el nuevo 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;

Resultados:

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

Este no es un resultado sorprendente:el uso de una función escalar conduce a una penalización de rendimiento de un orden de magnitud, mientras que el TVF de varias declaraciones fue solo unas 4 veces peor. En múltiples pruebas, la función en línea siempre fue tan rápida o un milisegundo o dos más rápida que ninguna función.

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;

Resultados truncados, reordenados manualmente:

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

Una cosa importante a tener en cuenta aquí es que el tiempo en milisegundos para INSERT en el TVF de sentencias múltiples y la sentencia RETURN en la función escalar también se tienen en cuenta dentro de las SELECCIONES individuales, por lo que no tiene sentido simplemente sumar todas los tiempos.

Cronometrajes manuales

Y finalmente, los tiempos de la tabla #temp:

SELECT query = q, 
    time_milliseconds = DATEDIFF(millisecond, s, e) 
  FROM #t 
  ORDER BY ID;

Resultados:

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

Resultados interesantes adicionales aquí:el contenedor del procedimiento siempre tuvo algunos gastos generales, aunque la importancia de eso podría ser verdaderamente subjetiva.

Resumen

Mi punto aquí hoy fue simplemente mostrar el nuevo DMV en acción y establecer las expectativas correctamente:algunas métricas de rendimiento para las funciones seguirán siendo engañosas y algunas seguirán sin estar disponibles (o al menos será muy tedioso reconstruirlas por sí mismo). ).

Sin embargo, creo que este nuevo DMV cubre una de las piezas más grandes de monitoreo de consultas que SQL Server no tenía antes:que las funciones escalares a veces son invisibles para el rendimiento, porque la única forma confiable de identificar su uso era analizar el texto de la consulta, que está lejos de ser infalible. No importa el hecho de que eso no le permitirá aislar su impacto en el rendimiento, o que tendría que haberlo sabido para buscar UDF escalares en el texto de la consulta en primer lugar.

Apéndice

Adjunto el script:DMExecFunctionStats.zip

Además, a partir de CTP1, aquí está el conjunto de columnas:

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

Columnas actualmente en sys.dm_exec_function_stats