Дефинираните от потребителя функции в SQL Server (UDF) са ключови обекти, с които всеки разработчик трябва да е наясно. Въпреки че са много полезни в много сценарии (клаузи WHERE, изчислени колони и ограничения за проверка), те все още имат някои ограничения и лоши практики, които могат да причинят проблеми с производителността. UDF с множество изрази може да окажат значително въздействие върху производителността и тази статия ще обсъди конкретно тези сценарии.
Функциите не се изпълняват по същия начин, както в обектно-ориентираните езици, въпреки че вградените функции с таблично стойности могат да се използват в сценарии, когато имате нужда от параметризирани изгледи, това не се отнася за функциите, които връщат скалари или таблици. Тези функции трябва да се използват внимателно, тъй като могат да причинят много проблеми с производителността. Те обаче са от съществено значение в много случаи, така че ще трябва да обърнем повече внимание на тяхното прилагане. Функциите се използват в SQL изразите в пакети, процедури, тригери или изгледи, вътре в ad-hoc SQL заявки или като част от заявки за отчитане, генерирани от инструменти като PowerBI или Tableau, в изчислени полета и ограничения за проверка. Докато скаларните функции могат да бъдат рекурсивни до 32 нива, табличните функции не поддържат рекурсия.
Типове функции в SQL Server
В SQL Server имаме три типа функции:дефинирани от потребителя скаларни функции (SFs), които връщат една скаларна стойност, потребителски дефинирани функции с таблично стойности (TVFs), които връщат таблица, и вградени функции с стойност на таблица (ITVFs), които нямат функционално тяло. Функциите на таблицата могат да бъдат вградени или многоизложени. Вградените функции нямат променливи за връщане, те просто връщат функции за стойност. Функциите с множество изрази се съдържат в блокове BEGIN-END код и могат да имат множество T-SQL изрази, които не създават никакви странични ефекти (като промяна на съдържание в таблица).
Ще покажем всеки тип функция в прост пример:
/**
inline table function
**/
CREATE FUNCTION dbo.fnInline( @P1 INT, @P2 VARCHAR(50) )
RETURNS TABLE
AS
RETURN ( SELECT @P1 AS P_OUT_1, @P2 AS P_OUT_2 )
/**
multi-statement table function
**/
CREATE FUNCTION dbo.fnMultiTable( @P1 INT, @P2 VARCHAR(50) )
RETURNS @r_table TABLE ( OUT_1 INT, OUT_2 VARCHAR(50) )
AS
BEGIN
INSERT @r_table SELECT @P1, @P2;
RETURN;
END;
/**
scalar function
**/
CREATE FUNCTION dbo.fnScalar( @P1 INT, @P2 INT )
RETURNS INT
AS
BEGIN
RETURN @P1 + @P2
END
Ограничения на функциите на SQL сървъра
Както бе споменато във въведението, има някои ограничения в използването на функциите и ще разгледам само няколко по-долу. Пълен списък може да бъде намерен в Microsoft Docs :
- Няма концепция за временни функции
- Не можете да създадете функция в друга база данни, но в зависимост от вашите привилегии имате достъп до нея
- С UDF не ви е позволено да извършвате никакви действия, които променят състоянието на базата данни,
- Вътре в UDF не можете да извикате процедура, освен разширената съхранена процедура
- UDF не може да върне набор от резултати, а само тип данни на таблица
- Не можете да използвате динамичен SQL или временни таблици в UDFs
- UDF са ограничени във възможностите за обработка на грешки – те не поддържат RAISERROR, нито TRY…CATCH и не можете да получите данни от системната променлива @ERROR
Какво е разрешено във функциите с множество изрази?
Разрешени са само следните неща:
- Изявления за присвояване
- Всички оператори за контрол на потока, с изключение на блока TRY…CATCH
- DECLARE извиквания, използвани за създаване на локални променливи и курсори
- Можете да използвате SELECT заявки, които имат списъци с изрази и да присвоите тези стойности на локално декларирани променливи
- Курсорите могат да препращат само към локални таблици и трябва да се отварят и затварят в тялото на функцията. FETCH може само да присвоява или променя стойности на локални променливи, не може да извлича или променя данни от базата данни
Какво трябва да се избягва при функциите с множество изрази, въпреки че е разрешено?
- Трябва да избягвате сценарии, при които използвате изчислени колони със скаларни функции – това ще доведе до повторно изграждане на индекси и бавни актуализации, които изискват преизчисления
- Имайте предвид, че всяка функция с множество изрази има своя план за изпълнение и въздействие върху производителността
- UDF със стойност на таблица с множество изрази, ако се използва в SQL израз или израз за присъединяване, ще бъде бавен поради неоптималния план за изпълнение.
- Не използвайте скаларни функции в изрази WHERE и клаузи ON, освен ако не сте сигурни, че ще потърси малък набор от данни и този набор от данни ще остане малък в бъдеще
Имена и параметри на функции
Както всяко друго име на обект, имената на функциите трябва да отговарят на правилата за идентификатори и трябва да бъдат уникални в тяхната схема. Ако правите скаларни функции, можете да ги стартирате с помощта на оператора EXECUTE. В този случай не е нужно да поставяте името на схемата в името на функцията. Вижте примера за извикването на функция EXECUTE по-долу (ние създаваме функция, която връща появата на N-ти ден в месеца и след това извлича тези данни):
CREATE FUNCTION dbo.fnGetDayofWeekInMonth
(
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurrence, 3 for the third
)
RETURNS DATETIME
AS
BEGIN
RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0,
CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-
(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0,
CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
END
-- In SQL Server 2012 and later versions, you can use the EXECUTE command or the SELECT command to run a UDF, or use a standard approach
DECLARE @ret DateTime
EXEC @ret = fnGetDayofWeekInMonth '2020', 'Jan', 'Mon',2
SELECT @ret AS Third_Monday_In_January_2020
SELECT dbo.fnGetDayofWeekInMonth('2020', 'Jan', DEFAULT, DEFAULT)
AS 'Using default',
dbo.fnGetDayofWeekInMonth('2020', 'Jan', 'Mon', 2) AS 'explicit'
Можем да дефинираме настройки по подразбиране за параметрите на функциите, те трябва да бъдат с префикс „@“ и да отговарят на правилата за именуване на идентификатори. Параметрите могат да бъдат само константни стойности, те не могат да се използват в SQL заявки вместо таблици, изгледи, колони или други обекти на базата данни, а стойностите не могат да бъдат изрази, дори детерминирани. Всички типове данни са разрешени, с изключение на типа данни TIMESTAMP и не могат да се използват нескаларни типове данни, с изключение на параметри с таблично значение. При „стандартни“ извиквания на функции трябва да посочите атрибута DEFAULT, ако искате да дадете на крайния потребител възможността да направи параметър незадължителен. В новите версии, използвайки синтаксиса EXECUTE, това вече не е необходимо, просто не въвеждате този параметър в извикването на функцията. Ако използваме персонализирани типове таблици, те трябва да бъдат маркирани като ЧЕТЕНЕ, което означава, че не можем да променим първоначалната стойност във функцията, но те могат да се използват при изчисления и дефиниции на други параметри.
Ефективност на функциите на SQL сървъра
Последната тема, която ще разгледаме в тази статия, използвайки функциите от предишната глава, е изпълнението на функциите. Ще разширим тази функция и ще наблюдаваме времето за изпълнение и качеството на плановете за изпълнение. Започваме със създаване на други версии на функции и продължаваме с тяхното сравнение:
CREATE FUNCTION dbo.fnGetDayofWeekInMonthBound
(
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurrence, 3 for the third
)
RETURNS DATETIME
WITH SCHEMABINDING
AS
BEGIN
RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
END
GO
CREATE FUNCTION dbo.fnNthDayOfWeekOfMonthInline (
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurence, 3 for the third
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN (SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7 AS TheDate)
GO
CREATE FUNCTION dbo.fnNthDayOfWeekOfMonthTVF (
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurence, 3 for the third
)
RETURNS @When TABLE (TheDate DATETIME)
WITH schemabinding
AS
Begin
INSERT INTO @When(TheDate)
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
RETURN
end
GO
Създайте някои тестови обаждания и тестови случаи
Започваме с таблични версии:
SELECT * FROM dbo.fnNthDayOfWeekOfMonthTVF('2020','Feb','Tue',2)
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue',2)),113) FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
OUTER apply dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue',2)
Създаване на тестови данни:
IF EXISTS(SELECT * FROM tempdb.sys.tables WHERE name LIKE '#DataForTest%')
DROP TABLE #DataForTest
GO
SELECT *
INTO #DataForTest
FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
CROSS join (VALUES ('jan'),('feb'),('mar'),('apr'),('may'),('jun'),('jul'),('aug'),('sep'),('oct'),('nov'),('dec'))months(Themonth)
CROSS join (VALUES ('Mon'),('Tue'),('Wed'),('Thu'),('Fri'),('Sat'),('Sun'))day(TheDay)
CROSS join (VALUES (1),(2),(3),(4))nth(nth)
Изпълнение на теста:
DECLARE @TableLog TABLE (OrderVal INT IDENTITY(1,1), Reason VARCHAR(500), TimeOfEvent DATETIME2 DEFAULT GETDATE())
Начало на времето:
INSERT INTO @TableLog(Reason) SELECT 'Starting My_Section_of_code' --place at the start
Първо, не използваме никакъв тип функция, за да получим базова линия:
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0)+ (7*Nth)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0))
[email protected]@DateFirst+(CHARINDEX(TheDay,'FriThuWedTueMonSunSat')-1)/3)%7 AS TheDate
INTO #Test0
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Using the code entirely unwrapped';
Сега използваме вградена функция със стойност на таблица, кръстосано приложена:
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate
INTO #Test1
FROM #DataForTest
CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)
INSERT INTO @TableLog(Reason) SELECT 'Inline function cross apply'
Използваме вградена функция със стойност на таблица, кръстосано приложена:
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsDate
INTO #Test2
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Inline function Derived table'
За да сравним ненадеждни, ние използваме скаларна функция със свързване на схема:
SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonthBound(TheYear,TheMonth,TheDay,nth))itsdate
INTO #Test3
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Trusted (Schemabound) scalar function'
След това използваме скаларна функция без обвързване на схема:
SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonth(TheYear,TheMonth,TheDay,nth))itsdate
INTO #Test6
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Untrusted scalar function'
След това, функцията за таблица с множество изрази, получена:
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsdate
INTO #Test4
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'multi-statement table function derived'
Накрая таблицата с множество изрази се прилага кръстосано:
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate
INTO #Test5
FROM #DataForTest
CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)
INSERT INTO @TableLog(Reason) SELECT 'multi-statement cross APPLY'--where the routine you want to time ends
Избройте всички тайминги:
SELECT ending.Reason AS Test, DateDiff(ms, starting.TimeOfEvent,ending.TimeOfEvent) [AS Time (ms)] FROM @TableLog starting
INNER JOIN @TableLog ending ON ending.OrderVal=starting.OrderVal+1
DROP table #Test0
DROP table #Test1
DROP table #Test2
DROP table #Test3
DROP table #Test4
DROP table #Test5
DROP table #Test6
DROP TABLE #DataForTest
Таблицата по-горе ясно показва, че трябва да вземете предвид производителността спрямо функционалността, когато използвате дефинирани от потребителя функции.
Заключение
Функциите се харесват от много разработчици, най-вече защото са „логически конструкции“. Можете лесно да създавате тестови случаи, те са детерминирани и капсулиращи, те се интегрират добре с потока на SQL код и позволяват гъвкавост при параметризирането. Те са добър избор, когато трябва да внедрите сложна логика, която трябва да се направи върху по-малък или вече филтриран набор от данни, който ще трябва да използвате повторно в множество сценарии. Вградените изгледи на таблица могат да се използват в изгледи, които се нуждаят от параметри, особено от горните слоеве (приложения, насочени към клиента). От друга страна, скаларните функции са чудесни за работа с XML или други йерархични формати, тъй като могат да бъдат извикани рекурсивно.
Дефинираните от потребителя функции с множество оператори са чудесно допълнение към вашия набор от инструменти за разработка, но трябва да разберете как работят и какви са техните ограничения и предизвикателства пред производителността. Неправилното им използване може да унищожи производителността на всяка база данни, но ако знаете как да използвате тези функции, те могат да донесат много ползи за повторното използване на кода и капсулирането.