Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Funções definidas pelo usuário do SQL Server


As funções definidas pelo usuário no SQL Server (UDFs) são objetos-chave que cada desenvolvedor deve conhecer. Embora sejam muito úteis em muitos cenários (cláusulas WHERE, colunas computadas e restrições de verificação), elas ainda apresentam algumas limitações e práticas inadequadas que podem causar problemas de desempenho. UDFs de várias instruções podem incorrer em impactos significativos no desempenho, e este artigo discutirá especificamente esses cenários.

As funções não são implementadas da mesma forma que nas linguagens orientadas a objetos, embora as funções com valor de tabela embutidas possam ser usadas em cenários em que você precisa de visualizações parametrizadas, isso não se aplica às funções que retornam escalares ou tabelas. Essas funções precisam ser usadas com cuidado, pois podem causar muitos problemas de desempenho. No entanto, eles são essenciais em muitos casos, por isso precisaremos prestar mais atenção em suas implementações. As funções são usadas nas instruções SQL em lotes, procedimentos, gatilhos ou exibições, em consultas SQL ad-hoc ou como parte de consultas de relatórios geradas por ferramentas como PowerBI ou Tableau, em campos calculados e restrições de verificação. Enquanto as funções escalares podem ser recursivas até 32 níveis, as funções de tabela não suportam recursão.

Tipos de funções no SQL Server


No SQL Server, temos três tipos de função:funções escalares definidas pelo usuário (SFs) que retornam um único valor escalar, funções com valor de tabela definidas pelo usuário (TVFs) que retornam uma tabela e funções com valor de tabela inline (ITVFs) que não possuem corpo funcional. As Funções de Tabela podem ser Inline ou Multi-statement. As funções inline não possuem variáveis ​​de retorno, elas apenas retornam funções de valor. As funções de várias instruções estão contidas em blocos de código BEGIN-END e podem ter várias instruções T-SQL que não criam nenhum efeito colateral (como modificar o conteúdo em uma tabela).

Mostraremos cada tipo de função em um exemplo simples:
/**
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

Limitações de funções do SQL Server


Conforme mencionado na introdução, existem algumas limitações no uso de funções, e explorarei apenas algumas abaixo. Uma lista completa pode ser encontrada em Microsoft Docs :
  • Não há conceito de funções temporárias
  • Você não pode criar uma função em outro banco de dados, mas, dependendo de seus privilégios, você pode acessá-la
  • Com UDFs, você não tem permissão para realizar nenhuma ação que altere o estado do banco de dados,
  • Dentro do UDF, você não pode chamar um procedimento, exceto o procedimento armazenado estendido
  • A UDF não pode retornar um conjunto de resultados, mas apenas um tipo de dados de tabela
  • Você não pode usar SQL dinâmico ou tabelas temporárias em UDFs
  • As UDFs são limitadas em recursos de tratamento de erros – elas não suportam RAISERROR nem TRY…CATCH e você não pode obter dados da variável @ERROR do sistema

O que é permitido nas funções de várias instruções?


Apenas as seguintes coisas são permitidas:
  • Declarações de atribuição
  • Todas as instruções de controle de fluxo, exceto o bloco TRY…CATCH
  • Chamadas DECLARE, usadas para criar variáveis ​​e cursores locais
  • Você pode usar consultas SELECT que possuem listas com expressões e atribuir esses valores a variáveis ​​declaradas localmente
  • Os cursores podem referenciar apenas tabelas locais e devem ser abertos e fechados dentro do corpo da função. FETCH só pode atribuir ou alterar valores de variáveis ​​locais, não recuperar ou alterar dados do banco de dados

O que deve ser evitado em funções de várias instruções, embora permitido?

  • Você deve evitar cenários em que está usando colunas computadas com funções escalares - isso causará recompilações de índice e atualizações lentas que exigem recálculos
  • Considere que qualquer função de várias instruções tem seu plano de execução e impacto no desempenho
  • A UDF com valor de tabela de várias instruções, se usada na expressão SQL ou na instrução de junção, será lenta devido ao plano de execução não ideal
  • Não use funções escalares em instruções WHERE e cláusulas ON, a menos que você tenha certeza de que consultará um conjunto de dados pequeno e esse conjunto de dados permanecerá pequeno no futuro

Nomes e parâmetros das funções


Como qualquer outro nome de objeto, os nomes de função devem obedecer às regras de identificadores e devem ser exclusivos em seu esquema. Se você estiver criando funções escalares, poderá executá-las usando a instrução EXECUTE. Nesse caso, você não precisa colocar o nome do esquema no nome da função. Veja o exemplo da chamada da função EXECUTE abaixo (criamos uma função que retorna a ocorrência do enésimo dia em um mês e depois recupera esses dados):
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'

Podemos definir padrões para parâmetros de função, eles devem ser prefixados com “@” e estar em conformidade com as regras de nomenclatura de identificadores. Os parâmetros podem ser apenas valores constantes, não podem ser usados ​​em consultas SQL em vez de tabelas, exibições, colunas ou outros objetos de banco de dados e os valores não podem ser expressões, mesmo determinísticas. Todos os tipos de dados são permitidos, exceto o tipo de dados TIMESTAMP, e nenhum tipo de dados não escalar pode ser usado, exceto para parâmetros com valor de tabela. Em chamadas de função “padrão”, você deve especificar o atributo DEFAULT se desejar dar ao usuário final a capacidade de tornar um parâmetro opcional. Nas novas versões, usando a sintaxe EXECUTE, isso não é mais necessário, você simplesmente não insere esse parâmetro na chamada da função. Se estivermos usando tipos de tabela personalizados, eles devem ser marcados como READONLY, o que significa que não podemos alterar o valor inicial dentro da função, mas podem ser usados ​​em cálculos e definições de outros parâmetros.

Desempenho da função do SQL Server


O último tópico que abordaremos neste artigo, usando funções do capítulo anterior, é o desempenho da função. Vamos estender essa função e monitorar os tempos de execução e a qualidade dos planos de execução. Começamos criando outras versões de funções e continuamos com a comparação:
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

Crie algumas chamadas de teste e casos de teste

Começamos com versões de tabela:
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)

Criando dados de teste:
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)

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

Início da cronometragem:
INSERT INTO @TableLog(Reason) SELECT 'Starting My_Section_of_code' --place at the start

Primeiro, não usamos nenhum tipo de função para obter uma linha de 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';

Agora usamos uma função inline com valor de tabela aplicada de forma cruzada:
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'

Usamos uma função inline com valor de tabela aplicada de forma cruzada:
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'

Para comparar não confiáveis, usamos uma função escalar com 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'
 

Em seguida, usamos uma função escalar sem vinculação de esquema:
SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonth(TheYear,TheMonth,TheDay,nth))itsdate
  INTO #Test6
  FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Untrusted scalar function'

Então, a função de tabela multi-instrução derivou:
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'

Por fim, a tabela multi-instrução foi aplicada de forma cruzada:
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

Liste todos os horários:
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

A tabela acima mostra claramente que você deve considerar o desempenho versus a funcionalidade ao usar funções definidas pelo usuário.

Conclusão


As funções são apreciadas por muitos desenvolvedores, principalmente porque são “construções lógicas”. Você pode criar facilmente casos de teste, eles são determinísticos e encapsulados, integram-se bem com o fluxo de código SQL e permitem flexibilidade na parametrização. Eles são uma boa opção quando você precisa implementar lógica complexa que precisa ser feita em um conjunto de dados menor ou já filtrado que você precisará reutilizar em vários cenários. As visualizações de tabela inline podem ser usadas em visualizações que precisam de parâmetros, especialmente das camadas superiores (aplicativos voltados para o cliente). Por outro lado, funções escalares são ótimas para trabalhar com XML ou outros formatos hierárquicos, pois podem ser chamadas recursivamente.

As funções de várias instruções definidas pelo usuário são um ótimo complemento para sua pilha de ferramentas de desenvolvimento, mas você precisa entender como elas funcionam e quais são suas limitações e desafios de desempenho. Seu uso errado pode destruir o desempenho de qualquer banco de dados, mas se você souber usar essas funções, elas podem trazer muitos benefícios para reutilização de código e encapsulamento.