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