Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

SQL Server 2016:sys.dm_exec_function_stats

In SQL Server 2016 CTP 2.1 è presente un nuovo oggetto visualizzato dopo CTP 2.0:sys.dm_exec_function_stats. Questo ha lo scopo di fornire funzionalità simili a sys.dm_exec_procedure_stats, sys.dm_exec_query_stats e sys.dm_exec_trigger_stats. Quindi ora è possibile tenere traccia delle metriche di runtime aggregate per le funzioni definite dall'utente.

O no?

Almeno in CTP 2.1, ho potuto ricavare qui solo metriche significative per le normali funzioni scalari:nulla è stato registrato per TVF inline o multi-statement. Non sono sorpreso delle funzioni inline, poiché sono essenzialmente espanse prima dell'esecuzione. Ma dal momento che i TVF con più dichiarazioni sono spesso problemi di prestazioni, speravo che si presentassero anche loro. Appaiono ancora in sys.dm_exec_query_stats, quindi puoi ancora derivare le loro metriche delle prestazioni da lì, ma può diventare complicato eseguire aggregazioni quando hai davvero più istruzioni che eseguono una parte del lavoro:nulla è arrotolato per te.

Diamo una rapida occhiata a come va a finire. Supponiamo di avere una tabella semplice con 100.000 righe:

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;

Volevo confrontare cosa succede quando esaminiamo le UDF scalari, le funzioni con valori di tabella multi-istruzione e le funzioni con valori di tabella inline e come vediamo quale lavoro è stato svolto in ciascun caso. Per prima cosa, immagina qualcosa di banale che possiamo fare in SELECT clausola, ma che potremmo voler dividere in compartimenti, come formattare una data come una stringa:

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

(Assegno l'output a una variabile, che forza la scansione dell'intera tabella, ma impedisce che le metriche delle prestazioni siano influenzate dagli sforzi di SSMS per consumare e visualizzare l'output. Grazie per il promemoria, Mikael Eriksson.)

Molte volte vedrai persone mettere quella conversione in una funzione e può essere scalare o TVF, come queste:

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

Ho creato wrapper di procedure attorno a queste funzioni come segue:

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

(E no, il dt_ la convenzione che stai vedendo non è una novità che penso sia una buona idea, era solo il modo più semplice per isolare tutte queste query nei DMV da tutto il resto che veniva raccolto. Ha anche semplificato l'aggiunta di suffissi per distinguere facilmente tra la query all'interno della procedura memorizzata e la versione ad hoc.)

Successivamente, ho creato una tabella #temp per memorizzare i tempi e ripetuto questo processo (eseguendo due volte la stored procedure, e due volte il corpo della procedura come query ad hoc isolata e tenendo traccia dei tempi di ciascuno):

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

Quindi ho eseguito alcune query diagnostiche ed ecco i risultati:

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;

Risultati:

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

Non è un errore di battitura; solo l'UDF scalare mostra qualsiasi presenza nel nuovo 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;

Risultati:

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

Questo non è un risultato sorprendente:l'uso di una funzione scalare porta a una penalizzazione delle prestazioni dell'ordine di grandezza, mentre il TVF multi-istruzione era solo circa 4 volte peggiore. Su più test, la funzione inline è sempre stata veloce o uno o due millisecondi più veloce di nessuna funzione.

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;

Risultati troncati, riordinati 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 da notare qui è che il tempo in millisecondi per INSERT nell'istruzione multi-istruzione TVF e l'istruzione RETURN nella funzione scalare vengono considerati anche all'interno dei singoli SELECT, quindi non ha senso sommare semplicemente tutto gli orari.

Cronometraggi manuali

E poi, infine, i tempi dalla #temp table:

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

Risultati:

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

Ulteriori risultati interessanti qui:il wrapper della procedura ha sempre avuto un sovraccarico, anche se quanto sia significativo potrebbe essere veramente soggettivo.

Riepilogo

Il mio punto qui oggi era semplicemente quello di mostrare il nuovo DMV in azione e impostare correttamente le aspettative:alcune metriche delle prestazioni per le funzioni saranno ancora fuorvianti e alcune non saranno ancora disponibili (o almeno sarà molto noioso da mettere insieme per te stesso ).

Penso che questo nuovo DMV copra uno dei più grandi pezzi di monitoraggio delle query che prima mancava a SQL Server:che le funzioni scalari a volte sono killer invisibili delle prestazioni, perché l'unico modo affidabile per identificarne l'utilizzo era analizzare il testo della query, che è tutt'altro che infallibile. Non importa il fatto che ciò non ti consentirà di isolare il loro impatto sulle prestazioni o che avresti dovuto sapere di cercare le UDF scalari nel testo della query in primo luogo.

Appendice

Ho allegato lo script:DMExecFunctionStats.zip

Inoltre, a partire da CTP1, ecco l'insieme di colonne:

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

Colonne attualmente in sys.dm_exec_function_stats