В SQL Server 2016 CTP 2.1 има един нов обект, който се появи след CTP 2.0:sys.dm_exec_function_stats. Това е предназначено да осигури подобна функционалност на sys.dm_exec_procedure_stats, sys.dm_exec_query_stats и sys.dm_exec_trigger_stats. Така че вече е възможно да се проследяват обобщени показатели по време на изпълнение за дефинирани от потребителя функции.
Или е така?
Поне в CTP 2.1 можех да изведа смислени метрики тук само за обикновени скаларни функции – нищо не беше регистрирано за вградени или мултиинструктивни TVF. Не съм изненадан от вградените функции, тъй като те така или иначе са разширени преди изпълнение. Но тъй като TVF с множество оператори често са проблеми с производителността, се надявах и те да се появят. Те все още се появяват в sys.dm_exec_query_stats, така че все още можете да извличате показателите им за ефективност оттам, но може да стане трудно да извършвате агрегирания, когато наистина имате множество изрази, които изпълняват част от работата – нищо не е събрано за вас.
Нека да разгледаме набързо как се случва това. Да приемем, че имаме проста таблица със 100 000 реда:
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;
Исках да сравня какво се случва, когато изследваме скаларни UDF, функции със стойност на таблици с множество оператори и вградени функции с стойност на таблица и как виждаме каква работа е извършена във всеки отделен случай. Първо, представете си нещо тривиално, което можем да направим в SELECT
клауза, но може да искаме да разделим, като форматиране на дата като низ:
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
(Присвоявам изхода на променлива, което принуждава цялата таблица да бъде сканирана, но не позволява на показателите за производителност да бъдат повлияни от усилията на SSMS да консумира и изобразява изхода. Благодаря за напомнянето, Mikael Eriksson.)
Много пъти ще видите хора да поставят това преобразуване във функция и то може да бъде скаларно или TVF, като тези:
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
Създадох обвивки на процедури около тези функции, както следва:
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
(И не, dt_
Конвенцията, която виждате, не е нещо ново, което смятам за добра идея, това беше просто най-простият начин, по който можех да изолирам всички тези заявки в DMV от всичко останало, което се събира. Освен това улеснява добавянето на суфикси за лесно разграничаване между заявката в съхранената процедура и ad hoc версията.)
След това създадох таблица #temp за съхраняване на тайминга и повторих този процес (и двете изпълнявайки съхранената процедура два пъти, и изпълнявайки тялото на процедурата като изолирана ad hoc заявка два пъти, и проследяване на времето на всяка една):
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
След това направих някои диагностични заявки и ето резултатите:
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;
Резултати:
name execution_count time_milliseconds --------- --------------- ----------------- dt_Scalar 400000 1116
Това не е печатна грешка; само скаларният UDF показва присъствие в новия 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;
Резултати:
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
Това не е изненадващ резултат:използването на скаларна функция води до намаляване на производителността от порядък по големина, докато TVF с множество изявления е само около 4 пъти по-лош. По време на множество тестове, вградената функция винаги е била толкова бърза или с милисекунда или две по-бърза, отколкото никаква функция.
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;
Съкратени резултати, пренаредени ръчно:
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
Важно нещо, което трябва да се отбележи тук, е, че времето в милисекунди за INSERT в TVF с множество оператори и оператора RETURN в скаларната функция също се отчитат в рамките на отделните SELECT, така че няма смисъл просто да сумирате всички времето.
Ръчни синхронизации
И накрая, времето от таблицата #temp:
SELECT query = q, time_milliseconds = DATEDIFF(millisecond, s, e) FROM #t ORDER BY ID;
Резултати:
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
Допълнителни интересни резултати тук:обвивката на процедурата винаги е имала някои допълнителни разходи, въпреки че колко значимо е това може да е наистина субективно.
Резюме
Моята цел тук днес беше просто да покажа новия DMV в действие и да задам правилно очакванията – някои показатели за производителност за функциите все още ще бъдат подвеждащи, а някои все още няма да бъдат достъпни изобщо (или поне ще бъде много досадно да събирате заедно за себе си ).
Мисля обаче, че този нов DMV обхваща една от най-големите части за наблюдение на заявки, които SQL Server липсваше преди:че скаларните функции понякога са невидими убийци на производителността, тъй като единственият надежден начин да се идентифицира тяхното използване е да се анализира текстът на заявката, който далеч не е сигурно. Няма значение факта, че това няма да ви позволи да изолирате тяхното въздействие върху производителността или че на първо място трябва да сте знаели, че търсите скаларни UDF в текста на заявката.
Приложение
Прикачих скрипта:DMExecFunctionStats.zip
Също така, от CTP1, тук е наборът от колони:
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 |
Колоните, които в момента са в sys.dm_exec_function_stats