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

Funzioni definite dall'utente di SQL Server

Le funzioni definite dall'utente in SQL Server (UDF) sono oggetti chiave di cui ogni sviluppatore dovrebbe essere a conoscenza. Sebbene siano molto utili in molti scenari (clausole WHERE, colonne calcolate e vincoli di controllo), presentano ancora alcune limitazioni e pratiche scorrette che possono causare problemi di prestazioni. Le UDF con più dichiarazioni potrebbero avere un impatto significativo sulle prestazioni e questo articolo discuterà specificamente questi scenari.

Le funzioni non sono implementate allo stesso modo dei linguaggi orientati agli oggetti, sebbene le funzioni inline con valori di tabella possano essere utilizzate in scenari in cui sono necessarie viste parametrizzate, ciò non si applica alle funzioni che restituiscono scalari o tabelle. Queste funzioni devono essere utilizzate con attenzione poiché possono causare molti problemi di prestazioni. Tuttavia, sono essenziali in molti casi, quindi dovremo prestare maggiore attenzione alle loro implementazioni. Le funzioni vengono utilizzate nelle istruzioni SQL all'interno di batch, procedure, trigger o viste, all'interno di query SQL ad hoc o come parte di query di report generate da strumenti come PowerBI o Tableau, in campi calcolati e controllano i vincoli. Mentre le funzioni scalari possono essere ricorsive fino a 32 livelli, le funzioni tabella non supportano la ricorsione.

Tipi di funzioni in SQL Server

In SQL Server, abbiamo tre tipi di funzione:funzioni scalari definite dall'utente (SF) che restituiscono un singolo valore scalare, funzioni con valori di tabella (TVF) definite dall'utente che restituiscono una tabella e funzioni con valori di tabella inline (ITVF) che non hanno un corpo funzionale. Le funzioni tabella possono essere Inline o Multi-istruzione. Le funzioni inline non hanno variabili di ritorno, restituiscono solo funzioni di valore. Le funzioni a più istruzioni sono contenute nei blocchi di codice BEGIN-END e possono avere più istruzioni T-SQL che non creano effetti collaterali (come la modifica del contenuto in una tabella).

Mostreremo ogni tipo di funzione in un semplice esempio:

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

Limitazioni delle funzioni di SQL Server

Come accennato nell'introduzione, ci sono alcune limitazioni nell'utilizzo delle funzioni e ne esplorerò solo alcune di seguito. Un elenco completo è disponibile su Microsoft Docs :

  • Non esiste il concetto di funzioni temporanee
  • Non puoi creare una funzione in un altro database, ma, a seconda dei tuoi privilegi, puoi accedervi
  • Con le UDF, non sei autorizzato a eseguire azioni che modifichino lo stato del database,
  • All'interno di UDF, non puoi chiamare una procedura, eccetto la stored procedure estesa
  • UDF non può restituire un set di risultati, ma solo un tipo di dati tabella
  • Non puoi utilizzare SQL dinamico o tabelle temporanee nelle UDF
  • Gli UDF hanno capacità di gestione degli errori limitate:non supportano RAISERROR né TRY...CATCH e non è possibile ottenere dati dalla variabile @ERROR di sistema

Cosa è consentito nelle funzioni con più istruzioni?

Sono consentite solo le seguenti cose:

  • Dichiarazioni di incarico
  • Tutte le istruzioni di controllo del flusso, eccetto il blocco TRY...CATCH
  • Chiamate DECLARE, usate per creare variabili e cursori locali
  • Puoi utilizzare query SELECT che hanno elenchi con espressioni e assegnare questi valori a variabili dichiarate localmente
  • I cursori possono fare riferimento solo a tabelle locali e devono essere aperti e chiusi all'interno del corpo della funzione. FETCH può assegnare o modificare solo valori di variabili locali, non recuperare o modificare i dati del database

Cosa dovrebbe essere evitato nelle funzioni a più dichiarazioni, sebbene consentito?

  • Dovresti evitare gli scenari in cui stai utilizzando colonne calcolate con funzioni scalari:ciò causerà ricostruzioni dell'indice e aggiornamenti lenti che richiedono ricalcoli
  • Considera che qualsiasi funzione con più istruzioni abbia il suo piano di esecuzione e un impatto sulle prestazioni
  • UDF con valori di tabella a più istruzioni, se utilizzato nell'espressione SQL o nell'istruzione join sarà lento a causa del piano di esecuzione non ottimale
  • Non utilizzare le funzioni scalari nelle istruzioni WHERE e nelle clausole ON a meno che tu non sia sicuro che interrogherà un piccolo set di dati e quel set di dati rimarrà piccolo in futuro

Nomi e parametri delle funzioni

Come qualsiasi altro nome oggetto, i nomi delle funzioni devono essere conformi alle regole per gli identificatori e devono essere univoci all'interno del loro schema. Se stai creando funzioni scalari, puoi eseguirle utilizzando l'istruzione EXECUTE. In questo caso, non è necessario inserire il nome dello schema nel nome della funzione. Vedi l'esempio della chiamata alla funzione ESEGUI di seguito (creiamo una funzione che restituisce l'occorrenza dell'ennesimo giorno in un mese e quindi recupera questi dati):

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'

Possiamo definire valori predefiniti per i parametri di funzione, devono essere preceduti da "@" e conformi alle regole di denominazione degli identificatori. I parametri possono essere solo valori costanti, non possono essere utilizzati nelle query SQL al posto di tabelle, viste, colonne o altri oggetti di database e i valori non possono essere espressioni, nemmeno deterministiche. Sono consentiti tutti i tipi di dati, ad eccezione del tipo di dati TIMESTAMP, e non è possibile utilizzare tipi di dati non scalari, ad eccezione dei parametri con valori di tabella. Nelle chiamate di funzione "standard", è necessario specificare l'attributo DEFAULT se si desidera dare all'utente finale la possibilità di rendere un parametro opzionale. Nelle nuove versioni, utilizzando la sintassi EXECUTE, questo non è più necessario, semplicemente non si inserisce questo parametro nella chiamata di funzione. Se utilizziamo tipi di tabella personalizzati, devono essere contrassegnati come READONLY, il che significa che non possiamo modificare il valore iniziale all'interno della funzione, ma possono essere utilizzati nei calcoli e nelle definizioni di altri parametri.

Prestazioni delle funzioni di SQL Server

L'ultimo argomento che tratteremo in questo articolo, utilizzando le funzioni del capitolo precedente, è la prestazione della funzione. Estenderemo questa funzione e monitoreremo i tempi di esecuzione e la qualità dei piani di esecuzione. Iniziamo creando altre versioni di funzioni e proseguiamo con il loro confronto:

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

Crea alcune chiamate di prova e casi di prova

Iniziamo con le versioni da tavolo:

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)

Creazione dei dati di prova:

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)

Prestazioni di prova:

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

Inizio tempi:

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

Innanzitutto, non utilizziamo alcun tipo di funzione per ottenere una linea di base:

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

Ora utilizziamo una funzione inline con valori di tabella applicata in modo incrociato:

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'

Usiamo una funzione inline con valori di tabella applicata in modo incrociato:

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'

Per confrontare non attendibile, utilizziamo una funzione scalare con 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'
 

Successivamente, utilizziamo una funzione scalare senza associazione allo schema:

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

Quindi, la funzione della tabella a più istruzioni ha derivato:

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'

Infine, la tabella con più istruzioni è stata applicata in modo incrociato:

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

Elenca tutti gli orari:

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

La tabella sopra mostra chiaramente che dovresti considerare le prestazioni rispetto alla funzionalità quando utilizzi le funzioni definite dall'utente.

Conclusione

Le funzioni piacciono a molti sviluppatori, soprattutto perché sono "costrutti logici". Puoi creare facilmente casi di test, sono deterministici e incapsulanti, si integrano perfettamente con il flusso di codice SQL e consentono flessibilità nella parametrizzazione. Sono una buona scelta quando devi implementare una logica complessa che deve essere eseguita su un set di dati più piccolo o già filtrato che dovrai riutilizzare in più scenari. Le viste tabella in linea possono essere utilizzate nelle viste che richiedono parametri, in particolare dai livelli superiori (applicazioni rivolte al client). D'altra parte, le funzioni scalari sono ottime per lavorare con XML o altri formati gerarchici, poiché possono essere chiamate ricorsivamente.

Le funzioni multi-istruzione definite dall'utente sono un'ottima aggiunta al tuo stack di strumenti di sviluppo, ma devi capire come funzionano e quali sono i loro limiti e le sfide delle prestazioni. Il loro uso errato può distruggere le prestazioni di qualsiasi database, ma se sai come utilizzare queste funzioni, possono apportare molti vantaggi al riutilizzo del codice e all'incapsulamento.