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

SQL Server brugerdefinerede funktioner

Brugerdefinerede funktioner i SQL Server (UDF'er) er nøgleobjekter, som hver udvikler bør være opmærksom på. Selvom de er meget nyttige i mange scenarier (WHERE-klausuler, beregnede kolonner og kontrolbegrænsninger), har de stadig nogle begrænsninger og dårlig praksis, der kan forårsage ydeevneproblemer. UDF'er med flere sætninger kan have betydelige præstationspåvirkninger, og denne artikel vil specifikt diskutere disse scenarier.

Funktionerne er ikke implementeret på samme måde som i objektorienterede sprog, selvom inline tabelværdierede funktioner kan bruges i scenarier, hvor du har brug for parametriserede visninger, gælder dette ikke for de funktioner, der returnerer skalarer eller tabeller. Disse funktioner skal bruges omhyggeligt, da de kan forårsage en masse præstationsproblemer. De er dog væsentlige i mange tilfælde, så vi bliver nødt til at være mere opmærksomme på deres implementeringer. Funktioner bruges i SQL-sætningerne i batches, procedurer, triggere eller visninger, inde i ad-hoc SQL-forespørgsler eller som en del af rapporteringsforespørgsler genereret af værktøjer som PowerBI eller Tableau, i beregnede felter og kontrolbegrænsninger. Mens skalarfunktioner kan være rekursive op til 32 niveauer, understøtter tabelfunktioner ikke rekursion.

Typer af funktioner i SQL Server

I SQL Server har vi tre funktionstyper:brugerdefinerede skalarfunktioner (SF'er), der returnerer en enkelt skalarværdi, brugerdefinerede table-valued-funktioner (TVF'er), der returnerer en tabel, og inline table-valued-funktioner (ITVF'er), der har ingen funktion krop. Tabelfunktioner kan være Inline eller Multi-statement. Inline-funktioner har ikke returvariable, de returnerer blot værdifunktioner. Multi-sætningsfunktioner er indeholdt i BEGIN-END kodeblokke og kan have flere T-SQL-sætninger, der ikke skaber nogen bivirkninger (såsom ændring af indhold i en tabel).

Vi vil vise hver type funktion i et simpelt eksempel:

/**inline tabelfunktion**/CREATE FUNCTION dbo.fnInline( @P1 INT, @P2 VARCHAR(50) )RETURNERER TABLEASRETURN (VÆLG @P1 AS P_OUT_1, @P2 AS P_OUT_2 )/**multi -sætningstabelfunktion**/CREATE FUNCTION dbo.fnMultiTable( @P1 INT, @P2 VARCHAR(50) )RETURNER @r_table TABLE ( OUT_1 INT, OUT_2 VARCHAR(50) )AS BEGIN INSERT @r_table SELECT @P1, @P2; VEND TILBAGE; END;/**skalarfunktion**/CREATE FUNCTION dbo.fnScalar( @P1 INT, @P2 INT )RETURNERER INTASBEGIN RETURN @P1 + @P2END 

SQL-serverfunktionsbegrænsninger

Som nævnt i introduktionen er der nogle begrænsninger i funktionsbrug, og jeg vil udforske nogle få nedenfor. En komplet liste kan findes på Microsoft Docs :

  • Der er intet koncept for midlertidige funktioner
  • Du kan ikke oprette en funktion i en anden database, men afhængigt af dine privilegier kan du få adgang til den
  • Med UDF'er har du ikke tilladelse til at udføre handlinger, der ændrer databasetilstanden,
  • Inde i UDF kan du ikke kalde en procedure, undtagen den udvidede lagrede procedure
  • UDF kan ikke returnere et resultatsæt, men kun en tabeldatatype
  • Du kan ikke bruge dynamisk SQL eller midlertidige tabeller i UDF'er
  • UDF'er er begrænsede i fejlhåndteringsmuligheder - de understøtter ikke RAISERROR eller TRY...CATCH, og du kan ikke hente data fra systemets @ERROR-variabel

Hvad er tilladt i multi-sætningsfunktioner?

Kun følgende ting er tilladt:

  • Opgaveerklæringer
  • Alle flowkontroludsagn, undtagen TRY…CATCH-blokken
  • DECLARE-kald, bruges til at oprette lokale variabler og markører
  • Du kan bruge SELECT-forespørgsler, der har lister med udtryk og tildele disse værdier til lokalt erklærede variable
  • Markører kan kun referere til lokale tabeller og skal åbnes og lukkes inde i funktionsteksten. FETCH kan kun tildele eller ændre værdier af lokale variabler, ikke hente eller ændre databasedata

Hvad bør undgås i multi-erklæringsfunktioner, selvom det er tilladt?

  • Du bør undgå scenarier, hvor du bruger beregnede kolonner med skalarfunktioner – dette vil forårsage indeksgenopbygninger og langsomme opdateringer, der kræver genberegninger
  • Tænk på, at enhver funktion med flere erklæringer har sin eksekveringsplan og præstationspåvirkning
  • Tabelværdi-UDF med flere sætninger, hvis den bruges i SQL-udtryk eller join-sætning, vil den være langsom på grund af den ikke-optimale eksekveringsplan
  • Brug ikke skalarfunktioner i WHERE-sætninger og ON-sætninger, medmindre du er sikker på, at det vil forespørge på et lille datasæt, og at datasættet forbliver lille i fremtiden

Funktionsnavne og -parametre

Som alle andre objektnavne skal funktionsnavne overholde regler for identifikatorer og skal være unikke i deres skema. Hvis du laver skalarfunktioner, kan du køre dem ved at bruge EXECUTE-sætningen. I dette tilfælde behøver du ikke at sætte skemanavnet i funktionsnavnet. Se eksemplet på EXECUTE-funktionskaldet nedenfor (vi opretter en funktion, der returnerer forekomsten af ​​N. dag i en måned og derefter henter disse data):

CREATE FUNCTION dbo.fnGetDayofWeekInMonth ( @YearInput VARCHAR(50), @MonthInput VARCHAR(50), -- Engelske måneder ( 'Jan', 'Feb', ... ) @WeekDayInput VARCHAR(50)='Man', -- Man, Tir, Ons, Tor, Fre, Lør, Søn @CountN INT=1 -- 1 for den første dato, 2 for den anden forekomst, 3 for den tredje ) RETURER DATOTIME SOM BEGIN RETURN DATEADD( MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1 - (DATEDEL (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 -- I SQL Server 2012 og nyere versioner kan du bruge kommandoen EXECUTE eller SELECT-kommandoen til at køre en UDF, eller bruge en standardtilgangDECLARE @ret DateTimeEXEC @ret =fnGetDayofWeekInMonth ' 2020', 'Jan', 'Man',2SELECT @ret AS Third_Mond ay_In_January_2020 VÆLG dbo.fnGetDayofWeekInMonth('2020', 'Jan', DEFAULT, DEFAULT) AS 'Using default', dbo.fnGetDayofWeekInMonth('2020', 'Jan', 'Mon>plicit' 

Vi kan definere standardindstillinger for funktionsparametre, de skal have "@" foran og være i overensstemmelse med regler for navngivning af identifikatorer. Parametre kan kun være konstante værdier, de kan ikke bruges i SQL-forespørgsler i stedet for tabeller, visninger, kolonner eller andre databaseobjekter, og værdier kan ikke være udtryk, heller ikke deterministiske. Alle datatyper er tilladt, undtagen TIMESTAMP-datatypen, og ingen ikke-skalære datatyper kan bruges, bortset fra tabelværdiparametre. I "standard" funktionskald skal du angive DEFAULT-attributten, hvis du ønsker at give slutbrugeren mulighed for at gøre en parameter valgfri. I nye versioner, ved hjælp af EXECUTE-syntaksen, er dette ikke længere nødvendigt, du indtaster bare ikke denne parameter i funktionskaldet. Hvis vi bruger brugerdefinerede tabeltyper, skal de markeres som LÆSEKUN, hvilket betyder, at vi ikke kan ændre startværdien inde i funktionen, men de kan bruges i beregninger og definitioner af andre parametre.

SQL-serverfunktionsydelse

Det sidste emne, vi vil dække i denne artikel, ved hjælp af funktioner fra det forrige kapitel, er funktionsydelse. Vi vil udvide denne funktion og overvåge eksekveringstider og kvaliteten af ​​eksekveringsplaner. Vi starter med at lave andre funktionsversioner og fortsætter med deres sammenligning:

CREATE FUNCTION dbo.fnGetDayofWeekInMonthBound ( @YearInput VARCHAR(50), @MonthInput VARCHAR(50), -- Engelske måneder ( 'Jan', 'Feb', ... ) @WeekDayInput VARCHAR(50)='man', -- man, tirs, ons, tor, fre, lør, søn @CountN INT=1 -- 1 for den første dato, 2 for den anden forekomst, 3 for den tredje ) RETURER DATOTIME MED SKEMABINDING SOM BEGIN RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1 -(DATEDEL (WEEKDAY) , DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)) [email protected]@DateFirst+(CHARINDEX(@WeekDayInput ,'FriThuWedTueMonSunSat')-1)/3)%7 AFSLUT GOCREATE FUNCTION dbo.fnNthDayOfWeekOfMonthInline ( @YearInput VARCHAR(50), @MonthInput VARCHAR(50), -- Engelske måneder ( 'Jan', ...F ) @WeekDayInput VARCHAR(50)='man', -- man, tir, ons, tor, fre, lør, søn @CountN INT=1 -- 1 for den første dato, 2 for anden forekomst, 3 for den tredje ) RETURTABEL MED SCHEMABINDING SOM RETURN (SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@AntalN)-1 -(DATODEL (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)GOCREATE FUNCTION dbo.fnNthDayOfWeekOfMontharTVF(5), VARCHARnthIn5(0) ), -- Engelske måneder ( 'Jan', 'Feb', ... ) @WeekDayInput VARCHAR(50)='Man', -- Man, Tir, Ons, Tor, Fre, Lør, Søn @CountN INT=1 -- 1 for den første dato, 2 for den anden forekomst, 3 for den tredje ) 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 

Opret nogle testopkald og testcases

Vi starter med tabelversioner:

SELECT * FROM dbo.fnNthDayOfWeekOfMonthTVF('2020','Feb','Tue',2)SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMontharTVF,'FeY ,'tirs',2)),113) FRA (VÆRDIER ('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) YDRE anvende dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue' ,2) 

Oprettelse af testdata:

HVIS FINDER(VÆLG * FRA tempdb.sys.tables HVOR navn SOM '#DataForTest%') SLIP TABEL #DataForTestGOSELECT * INTO #DataForTest FRA (VÆRDIER ('2014'),('2015'),( '2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear) CROSS join (VALUES ('jan'),( 'feb'),('mar'),('apr'),('maj'),('jun'),('jul'),('aug'),('sep'),('okt. '),('nov'),('dec'))months(Themonth) CROSS join (VALUES ('man'),('tir'),('on'),('tor'),('fre '),('lør'),('søn'))dag(TheDay) CROSS join (VALUES (1),(2),(3),(4))nth(nth) 

Test ydeevne:

DECLARE @TableLog TABLE (OrderVal INT IDENTITY(1,1), Reason VARCHAR(500), TimeOfEvent DATETIME2 DEFAULT GETDATE()) 

Start af timing:

INSERT INTO @TableLog(Reason) VÆLG 'Starting My_Section_of_code' --placer ved starten 

For det første bruger vi ingen type funktion til at få en baseline:

SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0)+ (7*Nth)-1 -(DATEDEL (WEEKDAY) , DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0)) [email protected]@DateFirst+(CHARINDEX(TheDay,'FreThuWedTueMonSunSat')-1 )/3)%7 AS TheDate INTO #Test0 FRA #DataForTestINSERT INTO @TableLog(Reason) VÆLG 'Brug af koden helt uindpakket';

Vi bruger nu en indlejret tabelværdi-funktion krydsanvendt:

SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate INTO #Test1 FROM #DataForTest CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)Log INSERT INTOTeason 'Inline funktion kryds anvende' 

Vi bruger en indlejret tabelværdi-funktion krydsanvendt:

SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsDate INTO #Test2 FROM #DataForTest INSERT INTOGIN @ReaTable ) VÆLG 'Inline funktion Afledt tabel' 

For at sammenligne untrusted bruger vi en skalarfunktion med skemabinding:

SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonthBound(TheYear,TheMonth,TheDay,nth))itsdate INTO #Test3 FROM #DataForTestINSERT INTO @TableLog(Reason) SELECT 'TrustedsSchema-funktion) '  

Dernæst bruger vi en skalarfunktion uden skemabinding:

SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonth(TheYear,TheMonth,TheDay,nth))itsdate INTO #Test6 FROM #DataForTestINSERT INTO @TableLog(Reason) SELECT 'Untrusted scal' 

Derefter udledte multi-sætningstabelfunktionen:

SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsdate INTO #Test4 FROM #DataForTest INSERT INTO @Testable ) VÆLG 'multi-statement table function derived' 

Endelig krydsanvendte tabellen med flere sætninger:

SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate INTO #Test5 FROM #DataForTest CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)Log INSERT INTOTeason 'multi-statement cross APPLY'--hvor den rutine, du ønsker at time, slutter 

Liste over alle tidspunkter:

VÆLG ending.Reason AS Test, DateDiff(ms, starting.TimeOfEvent,ending.TimeOfEvent) [AS Time (ms)] FRA @TableLog startingINNER JOIN @TableLog, der slutter PÅ ending.OrderVal=starting.OrderVal+1 DROP tabel #Test0DROP tabel #Test1DROP tabel #Test2DROP tabel #Test3DROP tabel #Test4DROP tabel #Test5DROP tabel #Test6DROP TABLE #DataForTest 

Ovenstående tabel viser tydeligt, at du bør overveje ydeevne kontra funktionalitet, når du bruger brugerdefinerede funktioner.

Konklusion

Funktioner kan lide af mange udviklere, mest fordi de er "logiske konstruktioner". Du kan nemt lave testcases, de er deterministiske og indkapslende, de integrerer fint med SQL-kodeflowet og tillader fleksibilitet i parameterisering. De er et godt valg, når du skal implementere kompleks logik, der skal udføres på et mindre eller allerede filtreret datasæt, som du skal genbruge i flere scenarier. Inline tabelvisninger kan bruges i visninger, der har brug for parametre, især fra øvre lag (klientvendte applikationer). På den anden side er skalarfunktioner gode til at arbejde med XML eller andre hierarkiske formater, da de kan kaldes rekursivt.

Brugerdefinerede multi-sætningsfunktioner er en fantastisk tilføjelse til din udviklingsværktøjsstabel, men du skal forstå, hvordan de fungerer, og hvad deres begrænsninger og ydeevneudfordringer er. Deres forkerte brug kan ødelægge ydeevnen af ​​enhver database, men hvis du ved, hvordan du bruger disse funktioner, kan de medføre en masse fordele ved genbrug og indkapsling af kode.


  1. Fuld liste over tegnsæt understøttet af MariaDB

  2. Ydeevne overraskelser og antagelser:DATEADD

  3. mysql select-forespørgsel i et serialiseret array

  4. MariaDB JSON_SEARCH() Forklaret