Door de gebruiker gedefinieerde functies in SQL Server (UDF's) zijn belangrijke objecten waarvan elke ontwikkelaar op de hoogte moet zijn. Hoewel ze in veel scenario's erg handig zijn (WHERE-clausules, berekende kolommen en controlebeperkingen), hebben ze nog steeds enkele beperkingen en slechte praktijken die prestatieproblemen kunnen veroorzaken. Multi-statement UDF's kunnen aanzienlijke prestatie-effecten met zich meebrengen, en in dit artikel worden deze scenario's specifiek besproken.
De functies zijn niet op dezelfde manier geïmplementeerd als in objectgeoriënteerde talen, hoewel inline-tabelwaardefuncties kunnen worden gebruikt in scenario's waarin u geparametriseerde weergaven nodig hebt, dit is niet van toepassing op de functies die scalaire waarden of tabellen retourneren. Deze functies moeten zorgvuldig worden gebruikt, omdat ze veel prestatieproblemen kunnen veroorzaken. In veel gevallen zijn ze echter essentieel, dus we zullen meer aandacht moeten besteden aan de implementatie ervan. Functies worden gebruikt in de SQL-instructies in batches, procedures, triggers of views, in ad-hoc SQL-query's of als onderdeel van rapportagequery's die worden gegenereerd door tools zoals PowerBI of Tableau, in berekende velden en controlebeperkingen. Hoewel scalaire functies tot 32 niveaus recursief kunnen zijn, ondersteunen tabelfuncties geen recursie.
Soorten functies in SQL Server
In SQL Server hebben we drie functietypen:door de gebruiker gedefinieerde scalaire functies (SF's) die een enkele scalaire waarde retourneren, door de gebruiker gedefinieerde tabelwaardefuncties (TVF's) die een tabel retourneren, en inline tabelwaardefuncties (ITVF's) die hebben geen functielichaam. Tabelfuncties kunnen Inline of Multi-statement zijn. Inline-functies hebben geen retourvariabelen, ze retourneren alleen waardefuncties. Functies met meerdere instructies zijn opgenomen in BEGIN-END-codeblokken en kunnen meerdere T-SQL-instructies hebben die geen bijwerkingen veroorzaken (zoals het wijzigen van inhoud in een tabel).
We zullen elk type functie in een eenvoudig voorbeeld laten zien:
/**
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 Server-functiebeperkingen
Zoals vermeld in de inleiding, zijn er enkele beperkingen in het gebruik van functies, en ik zal er hieronder enkele bespreken. Een volledige lijst is te vinden op Microsoft Docs :
- Er is geen concept van tijdelijke functies
- Je kunt geen functie maken in een andere database, maar je hebt er wel toegang toe, afhankelijk van je privileges
- Met UDF's mag u geen acties uitvoeren die de databasestatus wijzigen,
- Binnen UDF kunt u geen procedure aanroepen, behalve de uitgebreide opgeslagen procedure
- UDF kan geen resultatenset retourneren, maar alleen een tabelgegevenstype
- U kunt geen dynamische SQL of tijdelijke tabellen gebruiken in UDF's
- UDF's hebben beperkte mogelijkheden voor foutafhandeling - ze ondersteunen RAISERROR noch TRY...CATCH niet en u kunt geen gegevens ophalen uit de systeemvariabele @ERROR
Wat is toegestaan in functies met meerdere instructies?
Alleen de volgende dingen zijn toegestaan:
- Opdrachtverklaringen
- Alle flow control-statements, behalve het TRY…CATCH-blok
- DECLARE-aanroepen, gebruikt om lokale variabelen en cursors te maken
- U kunt SELECT-query's gebruiken die lijsten met expressies hebben en deze waarden toewijzen aan lokaal gedeclareerde variabelen
- Cursors kunnen alleen verwijzen naar lokale tabellen en moeten worden geopend en gesloten in de hoofdtekst van de functie. FETCH kan alleen waarden van lokale variabelen toewijzen of wijzigen, geen databasegegevens ophalen of wijzigen
Wat moet worden vermeden in functies met meerdere verklaringen, hoewel toegestaan?
- U moet scenario's vermijden waarin u berekende kolommen met scalaire functies gebruikt - dit zal leiden tot het opnieuw opbouwen van de index en langzame updates die herberekeningen vereisen
- Bedenk dat elke functie met meerdere instructies zijn uitvoeringsplan en prestatie-impact heeft
- Multi-statement tabelwaarde UDF, indien gebruikt in SQL-expressie of join-instructie zal traag zijn vanwege het niet-optimale uitvoeringsplan
- Gebruik geen scalaire functies in WHERE-instructies en ON-clausules, tenzij u zeker weet dat het een kleine dataset zal opvragen en die dataset in de toekomst klein zal blijven
Functienamen en parameters
Net als elke andere objectnaam moeten functienamen voldoen aan regels voor id's en moeten ze uniek zijn binnen hun schema. Als u scalaire functies maakt, kunt u deze uitvoeren met de instructie EXECUTE. In dit geval hoeft u de schemanaam niet in de functienaam te zetten. Zie het voorbeeld van de EXECUTE-functieaanroep hieronder (we maken een functie die het optreden van de N-de dag in een maand retourneert en vervolgens deze gegevens ophaalt):
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'
We kunnen standaardwaarden voor functieparameters definiëren, ze moeten worden voorafgegaan door "@" en voldoen aan de naamgevingsregels voor ID's. Parameters kunnen alleen constante waarden zijn, ze kunnen niet worden gebruikt in SQL-query's in plaats van tabellen, views, kolommen of andere database-objecten, en waarden kunnen geen expressies zijn, zelfs niet deterministische. Alle gegevenstypen zijn toegestaan, behalve het gegevenstype TIMESTAMP, en er kunnen geen niet-scalaire gegevenstypen worden gebruikt, behalve parameters met tabelwaarde. In "standaard" functie-aanroepen moet u het DEFAULT-attribuut specificeren als u de eindgebruiker de mogelijkheid wilt geven om een parameter optioneel te maken. In nieuwe versies, met behulp van de EXECUTE-syntaxis, is dit niet langer vereist, u voert deze parameter gewoon niet in de functieaanroep in. Als we aangepaste tabeltypen gebruiken, moeten deze worden gemarkeerd als ALLEEN LEZEN, wat betekent dat we de beginwaarde binnen de functie niet kunnen wijzigen, maar ze kunnen worden gebruikt in berekeningen en definities van andere parameters.
SQL Server Functie Prestaties
Het laatste onderwerp dat we in dit artikel zullen behandelen, met behulp van functies uit het vorige hoofdstuk, is functieprestaties. We breiden deze functie uit en bewaken de uitvoeringstijden en de kwaliteit van uitvoeringsplannen. We beginnen met het maken van andere functieversies en gaan verder met hun vergelijking:
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
Maak een aantal testoproepen en testcases
We beginnen met tafelversies:
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)
Testgegevens maken:
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)
Testprestaties:
DECLARE @TableLog TABLE (OrderVal INT IDENTITY(1,1), Reason VARCHAR(500), TimeOfEvent DATETIME2 DEFAULT GETDATE())
Begin van de timing:
INSERT INTO @TableLog(Reason) SELECT 'Starting My_Section_of_code' --place at the start
Ten eerste gebruiken we geen type functie om een basislijn te krijgen:
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';
We gebruiken nu een inline tabelwaardefunctie cross-applied:
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'
We gebruiken een inline tabelwaarde-functie die kruiselings wordt toegepast:
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'
Om niet-vertrouwd te vergelijken, gebruiken we een scalaire functie met schemabinding:
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'
Vervolgens gebruiken we een scalaire functie zonder schemabinding:
SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonth(TheYear,TheMonth,TheDay,nth))itsdate
INTO #Test6
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Untrusted scalar function'
Vervolgens is de multi-statement tabelfunctie afgeleid:
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'
Ten slotte werd de multi-statementtabel kruiselings toegepast:
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
Maak een lijst van alle tijden:
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
De bovenstaande tabel laat duidelijk zien dat u rekening moet houden met prestaties versus functionaliteit wanneer u door de gebruiker gedefinieerde functies gebruikt.
Conclusie
Functies zijn geliefd bij veel ontwikkelaars, vooral omdat het "logische constructies" zijn. U kunt eenvoudig testgevallen maken, ze zijn deterministisch en inkapselend, ze integreren mooi met de SQL-codestroom en bieden flexibiliteit bij het parametreren. Ze zijn een goede keuze wanneer u complexe logica moet implementeren die moet worden uitgevoerd op een kleinere of reeds gefilterde dataset die u in meerdere scenario's opnieuw moet gebruiken. Inline tabelweergaven kunnen worden gebruikt in weergaven die parameters nodig hebben, met name van hogere lagen (clientgerichte toepassingen). Aan de andere kant zijn scalaire functies geweldig om met XML of andere hiërarchische formaten te werken, omdat ze recursief kunnen worden aangeroepen.
Door de gebruiker gedefinieerde multi-statementfuncties zijn een geweldige aanvulling op uw ontwikkeltoolstapel, maar u moet begrijpen hoe ze werken en wat hun beperkingen en prestatie-uitdagingen zijn. Het verkeerde gebruik ervan kan de prestaties van elke database vernietigen, maar als u weet hoe u deze functies moet gebruiken, kunnen ze veel voordelen opleveren voor hergebruik en inkapseling van code.