sql >> Databáze >  >> RDS >> Sqlserver

SQL Server Uživatelem definované funkce

Uživatelsky definované funkce v SQL Server (UDF) jsou klíčové objekty, kterých by si měl být vědom každý vývojář. Ačkoli jsou velmi užitečné v mnoha scénářích (klauzule WHERE, vypočítané sloupce a kontrolní omezení), stále mají určitá omezení a špatné postupy, které mohou způsobit problémy s výkonem. UDF s více příkazy mohou mít významný dopad na výkon a tento článek se bude konkrétně zabývat těmito scénáři.

Funkce nejsou implementovány stejným způsobem jako v objektově orientovaných jazycích, i když funkce s inline tabulkovou hodnotou lze použít ve scénářích, kdy potřebujete parametrizované pohledy, to neplatí pro funkce, které vracejí skaláry nebo tabulky. Tyto funkce je třeba používat opatrně, protože mohou způsobit mnoho problémů s výkonem. V mnoha případech jsou však zásadní, takže jejich implementacím budeme muset věnovat větší pozornost. Funkce se používají v příkazech SQL uvnitř dávek, procedur, spouštěčů nebo pohledů, uvnitř ad-hoc dotazů SQL nebo jako součást dotazů sestav generovaných nástroji jako PowerBI nebo Tableau, ve vypočítaných polích a kontrolních omezeních. Zatímco skalární funkce mohou být rekurzivní až do 32 úrovní, tabulkové funkce rekurzi nepodporují.

Typy funkcí v SQL Server

V SQL Server máme tři typy funkcí:uživatelem definované skalární funkce (SF), které vracejí jednu skalární hodnotu, uživatelsky definované funkce s hodnotou tabulky (TVF), které vracejí tabulku, a inline funkce s hodnotou tabulky (ITVF), které nemají funkční tělo. Tabulkové funkce mohou být vložené nebo s více příkazy. Inline funkce nemají návratové proměnné, pouze vrací funkce hodnot. Vícepříkazové funkce jsou obsaženy v blocích kódu BEGIN-END a mohou mít více příkazů T-SQL, které nevytvářejí žádné vedlejší efekty (jako je úprava obsahu v tabulce).

Každý typ funkce si ukážeme na jednoduchém příkladu:

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

Omezení funkcí SQL Server

Jak bylo zmíněno v úvodu, existují určitá omezení v používání funkcí a níže prozkoumám jen několik. Úplný seznam lze nalézt v Dokumentech Microsoftu :

  • Neexistuje žádná koncepce dočasných funkcí
  • Nemůžete vytvořit funkci v jiné databázi, ale v závislosti na vašich oprávněních k ní můžete přistupovat
  • S UDF nemáte povoleno provádět žádné akce, které mění stav databáze,
  • Uvnitř UDF nemůžete volat proceduru kromě rozšířené uložené procedury
  • UDF nemůže vrátit sadu výsledků, ale pouze datový typ tabulky
  • V UDF nelze používat dynamické SQL nebo dočasné tabulky
  • UDF mají omezené možnosti zpracování chyb – nepodporují RAISERROR ani TRY…CATCH a nemůžete získat data ze systémové proměnné @ERROR

Co je povoleno ve funkcích s více příkazy?

Jsou povoleny pouze následující věci:

  • Prohlášení o přidělení
  • Všechny příkazy řízení toku kromě bloku TRY…CATCH
  • Volání DECLARE, používané k vytváření místních proměnných a kurzorů
  • Můžete použít SELECT dotazy, které mají seznamy s výrazy, a přiřadit tyto hodnoty lokálně deklarovaným proměnným
  • Kurzory mohou odkazovat pouze na místní tabulky a musí být otevřeny a zavřeny uvnitř těla funkce. FETCH může pouze přiřazovat nebo měnit hodnoty lokálních proměnných, nikoli načítat ani měnit data databáze

Čemu je třeba se vyhnout u funkcí s více příkazy, i když je to povoleno?

  • Měli byste se vyhnout situacím, kdy používáte počítané sloupce se skalárními funkcemi – to způsobí opětovné sestavení indexu a pomalé aktualizace, které vyžadují přepočty
  • Zvažte, že každá funkce s více příkazy má svůj plán provádění a dopad na výkon
  • UDF s hodnotou tabulky s více příkazy, pokud se použije ve výrazu SQL nebo příkazu join, bude pomalý kvůli neoptimálnímu plánu provádění
  • Nepoužívejte skalární funkce v příkazech WHERE a klauzulích ON, pokud si nejste jisti, že se budou dotazovat na malou datovou sadu a tato datová sada zůstane malá i v budoucnu.

Názvy a parametry funkcí

Stejně jako jakýkoli jiný název objektu musí názvy funkcí splňovat pravidla pro identifikátory a musí být v rámci svého schématu jedinečné. Pokud vytváříte skalární funkce, můžete je spustit pomocí příkazu EXECUTE. V tomto případě nemusíte uvádět název schématu do názvu funkce. Viz příklad volání funkce EXECUTE níže (vytvoříme funkci, která vrátí výskyt N-tého dne v měsíci a poté načte tato 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'

Pro parametry funkcí můžeme definovat výchozí hodnoty, musí mít předponu „@“ a musí být v souladu s pravidly pro pojmenování identifikátorů. Parametry mohou být pouze konstantní hodnoty, nelze je použít v SQL dotazech místo tabulek, pohledů, sloupců nebo jiných databázových objektů a hodnoty nemohou být výrazy, a to ani deterministické. Jsou povoleny všechny datové typy s výjimkou datového typu TIMESTAMP a nelze použít žádné neskalární datové typy, s výjimkou parametrů s tabulkovou hodnotou. Ve „standardních“ voláních funkcí musíte zadat atribut DEFAULT, pokud chcete dát koncovému uživateli možnost nastavit parametr jako volitelný. V nových verzích používajících syntaxi EXECUTE to již není vyžadováno, pouze tento parametr nezadáváte do volání funkce. Pokud používáme vlastní typy tabulek, musí být označeny jako READONLY, což znamená, že nemůžeme změnit počáteční hodnotu uvnitř funkce, ale lze je použít ve výpočtech a definicích jiných parametrů.

Výkon funkcí SQL Server

Posledním tématem, kterému se v tomto článku budeme věnovat pomocí funkcí z předchozí kapitoly, je výkon funkcí. Tuto funkci rozšíříme a budeme sledovat doby realizace a kvalitu realizačních plánů. Začneme vytvořením dalších verzí funkcí a pokračujeme jejich porovnáním:

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

Vytvořte několik testovacích volání a testovacích případů

Začneme tabulkovými verzemi:

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)

Vytváření testovacích dat:

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)

Test výkonu:

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

Začátek měření času:

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

Za prvé, k získání základní linie nepoužíváme žádný typ funkce:

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

Nyní používáme křížově aplikovanou inline tabulkovou funkci:

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'

Používáme inline tabulkovou funkci s křížovou aplikací:

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'

Pro porovnání nedůvěryhodných používáme skalární funkci se 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ále použijeme skalární funkci bez vazby schématu:

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

Poté byla tabulková funkce s více příkazy odvozena:

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'

Nakonec byla křížově aplikována tabulka s více příkazy:

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

Vypište všechna časování:

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

Výše uvedená tabulka jasně ukazuje, že byste měli zvážit výkon versus funkčnost, když používáte uživatelem definované funkce.

Závěr

Funkce jsou oblíbené mnoha vývojáři, většinou proto, že se jedná o „logické konstrukce“. Můžete snadno vytvářet testovací případy, jsou deterministické a zapouzdřující, dobře se integrují s tokem kódu SQL a umožňují flexibilitu v parametrizaci. Jsou dobrou volbou, když potřebujete implementovat složitou logiku, kterou je třeba provést na menší nebo již filtrované datové sadě, kterou budete muset znovu použít ve více scénářích. Inline tabulkové pohledy lze použít v pohledech, které vyžadují parametry, zejména z vyšších vrstev (klientské aplikace). Na druhou stranu jsou skalární funkce skvělé pro práci s XML nebo jinými hierarchickými formáty, protože je lze volat rekurzivně.

Uživatelsky definované funkce s více příkazy jsou skvělým doplňkem vaší sady vývojových nástrojů, ale musíte pochopit, jak fungují a jaká jsou jejich omezení a problémy s výkonem. Jejich nesprávné použití může zničit výkon jakékoli databáze, ale pokud víte, jak tyto funkce používat, mohou přinést mnoho výhod pro opětovné použití kódu a zapouzdření.


  1. Příklady YEARWEEK() – MySQL

  2. JSON_REMOVE() – Odebrání dat z dokumentu JSON v MySQL

  3. MySQL VARCHAR Lengths a UTF-8

  4. 4 způsoby, jak najít řádky, které obsahují velká písmena v PostgreSQL