V SQL Server 2016 CTP 2.1 je jeden nový objekt, který se objevil po CTP 2.0:sys.dm_exec_function_stats. To je určeno k poskytování podobných funkcí jako sys.dm_exec_procedure_stats, sys.dm_exec_query_stats a sys.dm_exec_trigger_stats. Nyní je tedy možné sledovat agregované runtime metriky pro uživatelem definované funkce.
Nebo ano?
Přinejmenším v CTP 2.1 jsem zde mohl odvodit jakékoli smysluplné metriky pouze pro běžné skalární funkce – nic nebylo registrováno pro inline nebo multipříkazové TVF. Inline funkce mě nepřekvapují, protože jsou v podstatě před spuštěním stejně rozšířeny. Ale protože vícepříkazové TVF jsou často problémy s výkonem, doufal jsem, že se také objeví. Stále se objevují v sys.dm_exec_query_stats, takže odtamtud můžete stále odvodit jejich metriky výkonu, ale může být obtížné provádět agregace, když opravdu máte více příkazů, které provádějí určitý podíl práce – nic pro vás není shrnuto.
Pojďme se rychle podívat, jak to dopadá. Řekněme, že máme jednoduchou tabulku se 100 000 řádky:
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;
Chtěl jsem porovnat, co se stane, když prozkoumáme skalární UDF, vícepříkazové funkce s tabulkovou hodnotou a funkce s inline tabulkou, a jak vidíme, jaká práce byla v každém případě vykonána. Nejprve si představte něco triviálního, co můžeme udělat v SELECT
klauzuli, ale můžeme ji chtít rozdělit, jako je formátování data jako řetězce:
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
(Výstup přiřadím proměnné, která vynutí skenování celé tabulky, ale zabrání tomu, aby byly metriky výkonu ovlivněny snahou SSMS spotřebovat a vykreslit výstup. Díky za připomenutí, Mikael Eriksson.)
Mnohokrát uvidíte, jak lidé převádějí tuto konverzi do funkce, a může to být skalární nebo TVF, jako jsou tyto:
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
Vytvořil jsem obálky procedur kolem těchto funkcí následovně:
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
(A ne, dt_
konvence, kterou vidíte, není nějaká nová věc, kterou považuji za dobrý nápad, byl to jen ten nejjednodušší způsob, jak jsem mohl izolovat všechny tyto dotazy v DMV od všeho ostatního, co se shromažďuje. Také to usnadnilo připojování přípon pro snadné rozlišení mezi dotazem v uložené proceduře a verzí ad hoc.)
Dále jsem vytvořil #temp tabulku pro ukládání časování a opakoval tento proces (jak dvakrát spustit uloženou proceduru, tak dvakrát spustit tělo procedury jako izolovaný ad hoc dotaz a sledovat načasování každého z nich):
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
Potom jsem provedl nějaké diagnostické dotazy a zde byly výsledky:
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;
Výsledky:
name execution_count time_milliseconds --------- --------------- ----------------- dt_Scalar 400000 1116
To není překlep; pouze skalární UDF vykazuje jakoukoli přítomnost v novém 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;
Výsledky:
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
To není překvapivý výsledek:použití skalární funkce vede k řádové penalizaci výkonu, zatímco vícepříkazová TVF byla jen asi 4x horší. Během několika testů byla inline funkce vždy stejně rychlá nebo o milisekundu nebo dvě rychlejší než žádná funkce.
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;
Zkrácené výsledky, přeřazené ručně:
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
Zde je důležité poznamenat, že čas v milisekundách pro INSERT ve vícepříkazovém TVF a příkaz RETURN ve skalární funkci se také počítá v rámci jednotlivých SELECTů, takže nemá smysl jen sčítat všechny načasování.
Ruční časování
A nakonec, časování z tabulky #temp:
SELECT query = q, time_milliseconds = DATEDIFF(millisecond, s, e) FROM #t ORDER BY ID;
Výsledky:
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
Další zajímavé výsledky zde:obálka procedur měla vždy určitou režii, i když její význam může být skutečně subjektivní.
Shrnutí
Chtěl jsem zde dnes pouze ukázat nový DMV v akci a správně nastavit očekávání – některé výkonnostní metriky funkcí budou stále zavádějící a některé stále nebudou dostupné vůbec (nebo bude přinejmenším velmi zdlouhavé seskládat pro sebe ).
Myslím si, že tento nový DMV pokrývá jednu z největších částí monitorování dotazů, které SQL Server předtím postrádal:že skalární funkce jsou někdy neviditelnými zabijáky výkonu, protože jediným spolehlivým způsobem, jak identifikovat jejich použití, bylo analyzovat text dotazu, který je daleko od spolehlivého. Nevadí, že vám to nedovolí izolovat jejich dopad na výkon, nebo že byste museli vědět, že v textu dotazu hledáte skalární UDF.
Příloha
Připojil jsem skript:DMExecFunctionStats.zip
Od CTP1 je zde také sada sloupců:
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 |
Sloupce aktuálně v sys.dm_exec_function_stats