Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Benutzerdefinierte SQL Server-Funktionen

Benutzerdefinierte Funktionen in SQL Server (UDFs) sind Schlüsselobjekte, die jeder Entwickler kennen sollte. Obwohl sie in vielen Szenarien sehr nützlich sind (WHERE-Klauseln, berechnete Spalten und Prüfeinschränkungen), weisen sie dennoch einige Einschränkungen und schlechte Vorgehensweisen auf, die Leistungsprobleme verursachen können. UDFs mit mehreren Anweisungen können erhebliche Auswirkungen auf die Leistung haben, und in diesem Artikel werden diese Szenarien speziell erörtert.

Die Funktionen werden nicht auf die gleiche Weise wie in objektorientierten Sprachen implementiert, obwohl Inline-Tabellenwertfunktionen in Szenarios verwendet werden können, in denen Sie parametrisierte Ansichten benötigen, gilt dies nicht für die Funktionen, die Skalare oder Tabellen zurückgeben. Diese Funktionen müssen sorgfältig verwendet werden, da sie viele Leistungsprobleme verursachen können. Sie sind jedoch in vielen Fällen unerlässlich, daher müssen wir ihrer Implementierung mehr Aufmerksamkeit schenken. Funktionen werden in den SQL-Anweisungen innerhalb von Stapeln, Prozeduren, Triggern oder Ansichten, innerhalb von Ad-hoc-SQL-Abfragen oder als Teil von Berichtsabfragen verwendet, die von Tools wie PowerBI oder Tableau, in berechneten Feldern und Prüfeinschränkungen generiert werden. Während Skalarfunktionen bis zu 32 Ebenen rekursiv sein können, unterstützen Tabellenfunktionen keine Rekursion.

Funktionstypen in SQL Server

In SQL Server gibt es drei Funktionstypen:benutzerdefinierte Skalarfunktionen (SFs), die einen einzelnen Skalarwert zurückgeben, benutzerdefinierte Tabellenwertfunktionen (TVFs), die eine Tabelle zurückgeben, und Inline-Tabellenwertfunktionen (ITVFs). haben keinen Funktionskörper. Tabellenfunktionen können Inline- oder Multi-Statement-Funktionen sein. Inline-Funktionen haben keine Rückgabevariablen, sie geben nur Wertfunktionen zurück. Funktionen mit mehreren Anweisungen sind in BEGIN-END-Codeblöcken enthalten und können mehrere T-SQL-Anweisungen haben, die keine Nebenwirkungen haben (z. B. das Ändern von Inhalten in einer Tabelle).

Wir zeigen jeden Funktionstyp in einem einfachen Beispiel:

/**
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

Einschränkungen der SQL Server-Funktion

Wie in der Einleitung erwähnt, gibt es einige Einschränkungen bei der Verwendung von Funktionen, und ich werde im Folgenden nur auf einige eingehen. Eine vollständige Liste finden Sie unter Microsoft Docs :

  • Es gibt kein Konzept für temporäre Funktionen
  • Sie können keine Funktion in einer anderen Datenbank erstellen, aber je nach Ihren Berechtigungen können Sie darauf zugreifen
  • Mit UDFs dürfen Sie keine Aktionen ausführen, die den Datenbankstatus ändern,
  • Innerhalb von UDF können Sie keine Prozedur aufrufen, außer der erweiterten gespeicherten Prozedur
  • UDF kann keine Ergebnismenge zurückgeben, sondern nur einen Tabellendatentyp
  • Sie können in UDFs weder dynamisches SQL noch temporäre Tabellen verwenden
  • UDFs sind in ihren Fähigkeiten zur Fehlerbehandlung eingeschränkt – sie unterstützen weder RAISERROR noch TRY…CATCH und Sie können keine Daten aus der Systemvariablen @ERROR abrufen

Was ist in Funktionen mit mehreren Anweisungen erlaubt?

Nur die folgenden Dinge sind erlaubt:

  • Zuweisungsanweisungen
  • Alle Anweisungen zur Flusssteuerung, außer dem TRY…CATCH-Block
  • DECLARE-Aufrufe, die verwendet werden, um lokale Variablen und Cursor zu erstellen
  • Sie können SELECT-Abfragen verwenden, die Listen mit Ausdrücken haben, und diese Werte lokal deklarierten Variablen zuweisen
  • Cursor können nur auf lokale Tabellen verweisen und müssen innerhalb des Funktionskörpers geöffnet und geschlossen werden. FETCH kann nur Werte von lokalen Variablen zuweisen oder ändern, keine Datenbankdaten abrufen oder ändern

Was sollte in Funktionen mit mehreren Anweisungen vermieden werden, obwohl sie erlaubt sind?

  • Sie sollten Szenarien vermeiden, in denen Sie berechnete Spalten mit Skalarfunktionen verwenden – dies führt zu Indexneuaufbauten und langsamen Aktualisierungen, die Neuberechnungen erfordern
  • Bedenken Sie, dass jede Funktion mit mehreren Anweisungen ihren Ausführungsplan und ihre Auswirkungen auf die Leistung hat
  • Tabellenwert-UDF mit mehreren Anweisungen ist aufgrund des nicht optimalen Ausführungsplans langsam, wenn sie in einem SQL-Ausdruck oder einer Join-Anweisung verwendet wird
  • Verwenden Sie keine Skalarfunktionen in WHERE-Anweisungen und ON-Klauseln, es sei denn, Sie sind sich sicher, dass ein kleiner Datensatz abgefragt wird und dieser Datensatz auch in Zukunft klein bleiben wird

Funktionsnamen und Parameter

Funktionsnamen müssen wie jeder andere Objektname den Regeln für Bezeichner entsprechen und innerhalb ihres Schemas eindeutig sein. Wenn Sie Skalarfunktionen erstellen, können Sie sie mit der EXECUTE-Anweisung ausführen. In diesem Fall müssen Sie den Schemanamen nicht in den Funktionsnamen einfügen. Sehen Sie sich das Beispiel des EXECUTE-Funktionsaufrufs unten an (wir erstellen eine Funktion, die das Vorkommen des N-ten Tages in einem Monat zurückgibt und dann diese Daten abruft):

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'

Wir können Standardwerte für Funktionsparameter definieren, ihnen muss ein „@“ vorangestellt werden und den Benennungsregeln für Bezeichner entsprechen. Parameter können nur konstante Werte sein, sie können nicht in SQL-Abfragen anstelle von Tabellen, Ansichten, Spalten oder anderen Datenbankobjekten verwendet werden, und Werte können keine Ausdrücke sein, auch nicht deterministische. Alle Datentypen sind zulässig, mit Ausnahme des TIMESTAMP-Datentyps, und es können keine nicht skalaren Datentypen verwendet werden, mit Ausnahme von Tabellenwertparametern. In „Standard“-Funktionsaufrufen müssen Sie das DEFAULT-Attribut angeben, wenn Sie dem Endbenutzer die Möglichkeit geben möchten, einen Parameter optional zu machen. In neuen Versionen, die die EXECUTE-Syntax verwenden, ist dies nicht mehr erforderlich, Sie geben diesen Parameter nur nicht im Funktionsaufruf ein. Wenn wir benutzerdefinierte Tabellentypen verwenden, müssen sie als READONLY markiert werden, was bedeutet, dass wir den Anfangswert innerhalb der Funktion nicht ändern können, aber sie können in Berechnungen und Definitionen anderer Parameter verwendet werden.

Leistung der SQL Server-Funktion

Das letzte Thema, das wir in diesem Artikel unter Verwendung von Funktionen aus dem vorherigen Kapitel behandeln werden, ist die Funktionsleistung. Wir werden diese Funktion erweitern und die Ausführungszeiten und die Qualität der Ausführungspläne überwachen. Wir beginnen mit der Erstellung anderer Funktionsversionen und fahren mit deren Vergleich fort:

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

Erstellen Sie einige Testanrufe und Testfälle

Wir beginnen mit Tabellenversionen:

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)

Testdaten erstellen:

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)

Testleistung:

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

Beginn der Zeitnahme:

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

Erstens verwenden wir keine Art von Funktion, um eine Grundlinie zu erhalten:

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

Wir verwenden jetzt eine übergreifend angewendete Inline-Tabellenwertfunktion:

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'

Wir verwenden eine übergreifende Inline-Tabellenwertfunktion:

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'

Um nicht vertrauenswürdig zu vergleichen, verwenden wir eine Skalarfunktion mit 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'
 

Als nächstes verwenden wir eine Skalarfunktion ohne Schemabindung:

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

Dann wird die Tabellenfunktion mit mehreren Anweisungen abgeleitet:

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'

Schließlich wurde die Multi-Statement-Tabelle übergreifend angewendet:

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

Listen Sie alle Zeiten auf:

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

Die obige Tabelle zeigt deutlich, dass Sie Leistung vs. Funktionalität abwägen sollten, wenn Sie benutzerdefinierte Funktionen verwenden.

Schlussfolgerung

Funktionen werden von vielen Entwicklern gemocht, vor allem weil sie „logische Konstrukte“ sind. Sie können Testfälle einfach erstellen, sie sind deterministisch und kapselnd, sie integrieren sich gut in den SQL-Codefluss und ermöglichen Flexibilität bei der Parametrisierung. Sie sind eine gute Wahl, wenn Sie komplexe Logik implementieren müssen, die für ein kleineres oder bereits gefiltertes Dataset ausgeführt werden muss, das Sie in mehreren Szenarien wiederverwenden müssen. Inline-Tabellenansichten können in Ansichten verwendet werden, die Parameter benötigen, insbesondere aus höheren Schichten (clientseitige Anwendungen). Andererseits eignen sich Skalarfunktionen hervorragend für die Arbeit mit XML oder anderen hierarchischen Formaten, da sie rekursiv aufgerufen werden können.

Benutzerdefinierte Funktionen mit mehreren Anweisungen sind eine großartige Ergänzung zu Ihrem Entwicklungstool-Stack, aber Sie müssen verstehen, wie sie funktionieren und welche Einschränkungen und Leistungsherausforderungen bestehen. Ihre falsche Verwendung kann die Leistung jeder Datenbank zerstören, aber wenn Sie wissen, wie man diese Funktionen verwendet, können sie viele Vorteile für die Wiederverwendung und Kapselung von Code bringen.