Sqlserver
 sql >> Baza danych >  >> RDS >> Sqlserver

Funkcje zdefiniowane przez użytkownika programu SQL Server

Funkcje zdefiniowane przez użytkownika w SQL Server (UDF) to kluczowe obiekty, o których każdy deweloper powinien wiedzieć. Chociaż są one bardzo przydatne w wielu scenariuszach (klauzule WHERE, kolumny obliczane i ograniczenia sprawdzające), nadal mają pewne ograniczenia i złe praktyki, które mogą powodować problemy z wydajnością. Wieloinstrukcyjne funkcje UDF mogą mieć znaczny wpływ na wydajność, a ten artykuł szczegółowo omówi te scenariusze.

Funkcje nie są implementowane w taki sam sposób, jak w językach obiektowych, chociaż wbudowane funkcje z wartościami przechowywanymi w tabeli mogą być używane w scenariuszach, w których potrzebne są widoki parametryczne, nie dotyczy to funkcji zwracających skalary lub tabele. Z funkcji tych należy korzystać ostrożnie, ponieważ mogą powodować wiele problemów z wydajnością. Jednak w wielu przypadkach są one niezbędne, dlatego będziemy musieli zwrócić większą uwagę na ich implementacje. Funkcje są używane w instrukcjach SQL w partiach, procedurach, wyzwalaczach lub widokach, w zapytaniach SQL ad hoc lub jako część zapytań raportowania generowanych przez narzędzia, takie jak PowerBI lub Tableau, w polach obliczeniowych i ograniczeniach sprawdzających. Chociaż funkcje skalarne mogą być rekurencyjne do 32 poziomów, funkcje tabelowe nie obsługują rekurencji.

Typy funkcji w SQL Server

W SQL Server mamy trzy typy funkcji:funkcje skalarne zdefiniowane przez użytkownika (SF), które zwracają pojedynczą wartość skalarną, funkcje z wartościami tabelarycznymi zdefiniowane przez użytkownika (TVF), które zwracają tabelę, oraz wbudowane funkcje z wartościami tabelarycznymi (ITVF), które nie mają ciała funkcji. Funkcje tabel mogą być wbudowane lub wielowyrazowe. Funkcje wbudowane nie mają zmiennych zwracanych, zwracają tylko funkcje wartości. Funkcje składające się z wielu instrukcji są zawarte w blokach kodu BEGIN-END i mogą zawierać wiele instrukcji T-SQL, które nie powodują żadnych skutków ubocznych (takich jak modyfikowanie zawartości tabeli).

Każdy typ funkcji pokażemy na prostym przykładzie:

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

Ograniczenia funkcji serwera SQL

Jak wspomniano we wstępie, istnieją pewne ograniczenia w korzystaniu z funkcji, a poniżej omówię tylko kilka. Pełną listę można znaleźć w Microsoft Docs :

  • Nie ma koncepcji funkcji tymczasowych
  • Nie możesz utworzyć funkcji w innej bazie danych, ale w zależności od uprawnień masz do niej dostęp
  • Dzięki UDF nie możesz wykonywać żadnych działań, które zmieniają stan bazy danych,
  • Wewnątrz UDF nie można wywołać procedury, z wyjątkiem rozszerzonej procedury składowanej
  • UDF nie może zwrócić zestawu wyników, a jedynie typ danych tabeli
  • Nie możesz używać dynamicznego SQL lub tabel tymczasowych w UDF
  • UDF mają ograniczone możliwości obsługi błędów – nie obsługują RAISERROR ani TRY…CATCH i nie można pobrać danych ze zmiennej systemowej @ERROR

Co jest dozwolone w funkcjach wielowyrazowych?

Dozwolone są tylko następujące rzeczy:

  • Oświadczenie o przypisaniu
  • Wszystkie instrukcje kontroli przepływu, z wyjątkiem bloku TRY…CATCH
  • Wywołania DECLARE, używane do tworzenia lokalnych zmiennych i kursorów
  • Możesz użyć zapytań SELECT, które mają listy z wyrażeniami i przypisać te wartości do zmiennych zadeklarowanych lokalnie
  • Kursory mogą odwoływać się tylko do tabel lokalnych i muszą być otwierane i zamykane w treści funkcji. FETCH może tylko przypisywać lub zmieniać wartości zmiennych lokalnych, a nie pobierać ani zmieniać danych z bazy danych

Czego należy unikać w funkcjach wielowyrazowych, chociaż jest to dozwolone?

  • Powinieneś unikać scenariuszy, w których używasz kolumn obliczeniowych z funkcjami skalarnymi – spowoduje to przebudowę indeksu i powolne aktualizacje, które wymagają ponownego obliczenia
  • Weź pod uwagę, że każda funkcja zawierająca wiele instrukcji ma swój plan wykonania i wpływ na wydajność
  • Wieloinstancyjny UDF z wartościami przechowywanymi w tabeli, jeśli zostanie użyty w wyrażeniu SQL lub instrukcji join, będzie powolny z powodu nieoptymalnego planu wykonania
  • Nie używaj funkcji skalarnych w instrukcjach WHERE i klauzulach ON, chyba że masz pewność, że wyśle ​​zapytanie do małego zestawu danych, a ten zestaw danych pozostanie mały w przyszłości

Nazwy i parametry funkcji

Jak każda inna nazwa obiektu, nazwy funkcji muszą być zgodne z regułami identyfikatorów i muszą być unikalne w ramach ich schematu. Jeśli tworzysz funkcje skalarne, możesz je uruchomić za pomocą instrukcji EXECUTE. W takim przypadku nie musisz umieszczać nazwy schematu w nazwie funkcji. Zobacz przykład wywołania funkcji EXECUTE poniżej (tworzymy funkcję, która zwraca wystąpienie N-tego dnia w miesiącu, a następnie pobiera te dane):

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'

Możemy zdefiniować wartości domyślne parametrów funkcji, muszą być one poprzedzone znakiem „@” i zgodne z zasadami nazewnictwa identyfikatorów. Parametry mogą być tylko wartościami stałymi, nie mogą być używane w zapytaniach SQL zamiast tabel, widoków, kolumn lub innych obiektów bazy danych, a wartości nie mogą być wyrażeniami, nawet deterministycznymi. Dozwolone są wszystkie typy danych, z wyjątkiem typu danych TIMESTAMP, i nie można używać nieskalarnych typów danych, z wyjątkiem parametrów wycenianych w tabeli. W „standardowych” wywołaniach funkcji musisz określić atrybut DEFAULT, jeśli chcesz dać użytkownikowi końcowemu możliwość uczynienia parametru opcjonalnym. W nowych wersjach, używając składni EXECUTE, nie jest to już wymagane, po prostu nie wpisujesz tego parametru w wywołaniu funkcji. Jeśli używamy niestandardowych typów tabel, muszą one być oznaczone jako READONLY, co oznacza, że ​​nie możemy zmienić początkowej wartości wewnątrz funkcji, ale można ich używać w obliczeniach i definiowaniu innych parametrów.

Wydajność funkcji serwera SQL

Ostatnim tematem, który omówimy w tym artykule, wykorzystując funkcje z poprzedniego rozdziału, jest wykonanie funkcji. Rozbudujemy tę funkcję i będziemy monitorować czas realizacji oraz jakość planów realizacji. Zaczynamy od stworzenia innych wersji funkcji i kontynuujemy ich porównywanie:

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

Utwórz kilka połączeń testowych i przypadków testowych

Zaczynamy od wersji stołowych:

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)

Tworzenie danych testowych:

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)

Wydajność testu:

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

Początek pomiaru czasu:

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

Po pierwsze, nie używamy żadnego typu funkcji, aby uzyskać linię bazową:

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

Używamy teraz wbudowanej funkcji o wartości tabeli, która ma zastosowanie krzyżowe:

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'

Używamy wbudowanej funkcji o wartościach z tabeli, która ma zastosowanie krzyżowe:

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'

Aby porównać niezaufane, używamy funkcji skalarnej z wiązaniem schematów:

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'
 

Następnie używamy funkcji skalarnej bez wiązania schematu:

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

Następnie wyprowadzono funkcję tabeli z wieloma instrukcjami:

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'

Wreszcie, tabela zawierająca wiele instrukcji została zastosowana krzyżowo:

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

Wymień wszystkie czasy:

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

Powyższa tabela wyraźnie pokazuje, że podczas korzystania z funkcji zdefiniowanych przez użytkownika należy rozważyć wydajność w porównaniu z funkcjonalnością.

Wniosek

Funkcje są lubiane przez wielu programistów, głównie dlatego, że są „konstrukcjami logicznymi”. Możesz łatwo tworzyć przypadki testowe, są deterministyczne i hermetyzujące, ładnie integrują się z przepływem kodu SQL i zapewniają elastyczność w parametryzacji. Są dobrym wyborem, gdy trzeba zaimplementować złożoną logikę, która musi być wykonana na mniejszym lub już przefiltrowanym zestawie danych, który trzeba będzie ponownie wykorzystać w wielu scenariuszach. Widoki tabeli wbudowanej mogą być używane w widokach, które wymagają parametrów, zwłaszcza z wyższych warstw (aplikacje skierowane do klienta). Z drugiej strony funkcje skalarne świetnie nadają się do pracy z XML lub innymi formatami hierarchicznymi, ponieważ można je wywoływać rekurencyjnie.

Zdefiniowane przez użytkownika funkcje wielu instrukcji są doskonałym dodatkiem do zestawu narzędzi programistycznych, ale musisz zrozumieć, jak działają, jakie są ich ograniczenia i wyzwania związane z wydajnością. Ich niewłaściwe użycie może zniszczyć wydajność dowolnej bazy danych, ale jeśli wiesz, jak korzystać z tych funkcji, mogą one przynieść wiele korzyści w zakresie ponownego wykorzystania i enkapsulacji kodu.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Zastąp pierwsze wystąpienie podciągu w ciągu w SQL

  2. Porównanie ciągów SQL uwzględniających wielkość liter

  3. DATETIME2FROMPARTS() Przykłady w SQL Server (T-SQL)

  4. Łączna suma na serwerze MS Sql

  5. Jak przekazać zmienną null do procedury składowanej SQL z kodu C#.net?