sql >> Databasteknik >  >> RDS >> Sqlserver

SQL Server Användardefinierade funktioner

Användardefinierade funktioner i SQL Server (UDF) är nyckelobjekt som varje utvecklare bör vara medveten om. Även om de är mycket användbara i många scenarier (WHERE-satser, beräknade kolumner och kontrollbegränsningar), har de fortfarande vissa begränsningar och dålig praxis som kan orsaka prestandaproblem. UDF:er med flera påståenden kan ha betydande prestandapåverkan, och den här artikeln kommer specifikt att diskutera dessa scenarier.

Funktionerna är inte implementerade på samma sätt som i objektorienterade språk, även om inline-tabellvärdade funktioner kan användas i scenarier när du behöver parametriserade vyer, detta gäller inte funktionerna som returnerar skalärer eller tabeller. Dessa funktioner måste användas noggrant eftersom de kan orsaka många prestandaproblem. Men de är viktiga i många fall, så vi kommer att behöva ägna mer uppmärksamhet åt deras implementeringar. Funktioner används i SQL-satser i batcher, procedurer, utlösare eller vyer, inuti ad-hoc SQL-frågor, eller som en del av rapporteringsfrågor som genereras av verktyg som PowerBI eller Tableau, i beräknade fält och kontrollbegränsningar. Medan skalära funktioner kan vara rekursiva upp till 32 nivåer, stöder inte tabellfunktioner rekursion.

Typer av funktioner i SQL Server

I SQL Server har vi tre funktionstyper:användardefinierade skalära funktioner (SFs) som returnerar ett enda skalärt värde, användardefinierade tabellvärderade funktioner (TVFs) som returnerar en tabell och inline-tabellvärdade funktioner (ITVFs) som har ingen funktionskropp. Tabellfunktioner kan vara inline eller multi-statement. Inline-funktioner har inga returvariabler, de returnerar bara värdefunktioner. Flersatsfunktioner finns i BEGIN-END-kodblock och kan ha flera T-SQL-satser som inte skapar några bieffekter (som att ändra innehåll i en tabell).

Vi kommer att visa varje typ av funktion i ett enkelt exempel:

/**
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-serverfunktionsbegränsningar

Som nämnts i inledningen finns det några begränsningar i funktionsanvändningen, och jag kommer att utforska några nedan. En komplett lista finns på Microsoft Docs :

  • Det finns inget koncept med tillfälliga funktioner
  • Du kan inte skapa en funktion i en annan databas, men beroende på dina privilegier kan du komma åt den
  • Med UDF:er får du inte utföra några åtgärder som ändrar databasstatus,
  • Inuti UDF kan du inte anropa en procedur, förutom den utökade lagrade proceduren
  • UDF kan inte returnera en resultatuppsättning, utan endast en tabelldatatyp
  • Du kan inte använda dynamisk SQL eller temporära tabeller i UDF:er
  • UDF:er är begränsade i felhanteringsmöjligheter – de stöder inte RAISERROR eller TRY...CATCH och du kan inte hämta data från systemets @ERROR-variabel

Vad är tillåtet i funktioner med flera uttalanden?

Endast följande saker är tillåtna:

  • Uppdragsbeskrivningar
  • Alla flödeskontrollsatser, förutom TRY...CATCH-blocket
  • DECLARE-anrop, används för att skapa lokala variabler och markörer
  • Du kan använda SELECT-frågor som har listor med uttryck och tilldela dessa värden till lokalt deklarerade variabler
  • Markörer kan endast referera till lokala tabeller och måste öppnas och stängas inuti funktionskroppen. FETCH kan bara tilldela eller ändra värden för lokala variabler, inte hämta eller ändra databasdata

Vad bör undvikas i funktioner med flera uttalanden, även om det är tillåtet?

  • Du bör undvika scenarier där du använder beräknade kolumner med skalära funktioner – detta kommer att orsaka indexombyggnader och långsamma uppdateringar som kräver omberäkningar
  • Tänk på att alla funktioner med flera uttalanden har sin exekveringsplan och resultatpåverkan
  • Flersatstabellvärderad UDF, om den används i SQL-uttryck eller join-sats kommer att vara långsam på grund av den icke-optimala exekveringsplanen
  • Använd inte skalära funktioner i WHERE-satser och ON-satser såvida du inte är säker på att det kommer att fråga efter en liten datamängd, och den datamängden kommer att förbli liten i framtiden

Funktionsnamn och parametrar

Liksom alla andra objektnamn måste funktionsnamn följa regler för identifierare och måste vara unika i deras schema. Om du gör skalära funktioner kan du köra dem genom att använda EXECUTE-satsen. I det här fallet behöver du inte ange schemanamnet i funktionsnamnet. Se exemplet på EXECUTE-funktionsanropet nedan (vi skapar en funktion som returnerar förekomsten av N:te dagen i en månad och sedan hämtar dessa data):

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'

Vi kan definiera standardvärden för funktionsparametrar, de måste ha prefixet "@" och överensstämma med reglerna för namngivning av identifierare. Parametrar kan endast vara konstanta värden, de kan inte användas i SQL-frågor istället för tabeller, vyer, kolumner eller andra databasobjekt, och värden kan inte vara uttryck, inte ens deterministiska. Alla datatyper är tillåtna, förutom datatypen TIMESTAMP, och inga icke-skalära datatyper kan användas, förutom tabellvärdade parametrar. I "standard" funktionsanrop måste du ange DEFAULT-attributet om du vill ge slutanvändaren möjligheten att göra en parameter valfri. I nya versioner, med hjälp av EXECUTE-syntaxen, krävs detta inte längre, du anger bara inte denna parameter i funktionsanropet. Om vi ​​använder anpassade tabelltyper måste de markeras som READONLY, vilket innebär att vi inte kan ändra det initiala värdet i funktionen, men de kan användas i beräkningar och definitioner av andra parametrar.

SQL-serverfunktionsprestanda

Det sista ämnet vi kommer att ta upp i den här artikeln, med hjälp av funktioner från föregående kapitel, är funktionsprestanda. Vi kommer att utöka denna funktion och övervaka genomförandetider och kvaliteten på genomförandeplaner. Vi börjar med att skapa andra funktionsversioner och fortsätter med deras jämförelse:

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

Skapa några testsamtal och testfall

Vi börjar med tabellversioner:

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)

Skapar testdata:

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)

Testprestanda:

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

Start av tidtagning:

INSERT INTO @TableLog(Reason) SELECT 'Starting My_Section_of_code' --place at the start

För det första använder vi ingen typ av funktion för att få en baslinje:

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';

Vi använder nu en inline tabellvärderad funktion korstillämpad:

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'

Vi använder en inline tabellvärderad funktion korstillämpad:

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'

För att jämföra otillförlitliga använder vi en skalär funktion med 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'
 

Därefter använder vi en skalär funktion utan schemabindning:

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

Sedan härledde tabellfunktionen med flera påståenden:

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'

Slutligen korsapplicerade tabellen med flera påståenden:

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

Lista ut alla tidpunkter:

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

Tabellen ovan visar tydligt att du bör överväga prestanda kontra funktionalitet när du använder användardefinierade funktioner.

Slutsats

Funktioner gillas av många utvecklare, mest för att de är "logiska konstruktioner". Du kan enkelt skapa testfall, de är deterministiska och inkapslande, de integrerar snyggt med SQL-kodflödet och tillåter flexibilitet i parametriseringen. De är ett bra val när du behöver implementera komplex logik som behöver göras på en mindre eller redan filtrerad datauppsättning som du måste återanvända i flera scenarier. Inline-tabellvyer kan användas i vyer som behöver parametrar, särskilt från övre skikt (klientvända applikationer). Å andra sidan är skalära funktioner bra att arbeta med XML eller andra hierarkiska format, eftersom de kan kallas rekursivt.

Användardefinierade funktioner med flera påståenden är ett bra tillägg till din utvecklingsverktygsstapel, men du måste förstå hur de fungerar och vilka deras begränsningar och prestandautmaningar är. Deras felaktiga användning kan förstöra prestandan för vilken databas som helst, men om du vet hur man använder dessa funktioner kan de ge många fördelar med kodåteranvändning och inkapsling.


  1. Hur ställer man in initialvärde och automatisk ökning i MySQL?

  2. Vad är PostgreSQL-motsvarigheten för ISNULL()

  3. Installera Oracle 32-bitars klient på Windows Server Kör redan 64-bitars Oracle Database Server

  4. Beställ en MySQL-tabell med två kolumner