sql >> Base de Datos >  >> RDS >> Sqlserver

Funciones definidas por el usuario de SQL Server

Las funciones definidas por el usuario en SQL Server (UDF) son objetos clave que todo desarrollador debe conocer. Aunque son muy útiles en muchos escenarios (cláusulas WHERE, columnas calculadas y restricciones de verificación), todavía tienen algunas limitaciones y malas prácticas que pueden causar problemas de rendimiento. Los UDF de múltiples declaraciones pueden tener impactos significativos en el rendimiento, y este artículo tratará específicamente estos escenarios.

Las funciones no se implementan de la misma manera que en los lenguajes orientados a objetos, aunque las funciones con valores de tabla en línea se pueden usar en escenarios cuando necesita vistas parametrizadas, esto no se aplica a las funciones que devuelven escalares o tablas. Estas funciones deben usarse con cuidado ya que pueden causar muchos problemas de rendimiento. Sin embargo, son esenciales en muchos casos, por lo que tendremos que prestar más atención a sus implementaciones. Las funciones se utilizan en las instrucciones SQL dentro de lotes, procedimientos, disparadores o vistas, dentro de consultas SQL ad-hoc o como parte de consultas de informes generadas por herramientas como PowerBI o Tableau, en campos calculados y restricciones de verificación. Mientras que las funciones escalares pueden ser recursivas hasta 32 niveles, las funciones de tabla no admiten la recursividad.

Tipos de funciones en SQL Server

En SQL Server, tenemos tres tipos de funciones:funciones escalares definidas por el usuario (SF) que devuelven un solo valor escalar, funciones con valores de tabla definidas por el usuario (TVF) que devuelven una tabla y funciones con valores de tabla en línea (ITVF) que no tienen función corporal. Las funciones de tabla pueden ser en línea o de varias declaraciones. Las funciones en línea no tienen variables de retorno, solo devuelven funciones de valor. Las funciones de instrucciones múltiples están contenidas en bloques de código BEGIN-END y pueden tener varias instrucciones T-SQL que no crean ningún efecto secundario (como modificar el contenido de una tabla).

Mostraremos cada tipo de función en un ejemplo simple:

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

Limitaciones de las funciones del servidor SQL

Como se mencionó en la introducción, existen algunas limitaciones en el uso de funciones, y exploraré solo algunas a continuación. Puede encontrar una lista completa en Microsoft Docs :

  • No existe el concepto de funciones temporales
  • No puede crear una función en otra base de datos, pero, dependiendo de sus privilegios, puede acceder a ella
  • Con UDF, no puede realizar ninguna acción que cambie el estado de la base de datos,
  • Dentro de UDF, no puede llamar a un procedimiento, excepto al procedimiento almacenado extendido
  • UDF no puede devolver un conjunto de resultados, sino solo un tipo de datos de tabla
  • No puede usar SQL dinámico o tablas temporales en UDF
  • Los UDF tienen capacidades limitadas de manejo de errores:no admiten RAISERROR ni TRY…CATCH y no puede obtener datos de la variable @ERROR del sistema

¿Qué está permitido en las funciones de varios estados de cuenta?

Solo se permiten las siguientes cosas:

  • Declaraciones de tareas
  • Todas las sentencias de control de flujo, excepto el bloque TRY…CATCH
  • Llamadas a DECLARE, usadas para crear variables locales y cursores
  • Puede usar consultas SELECT que tienen listas con expresiones y asignar estos valores a variables declaradas localmente
  • Los cursores solo pueden hacer referencia a tablas locales y deben abrirse y cerrarse dentro del cuerpo de la función. FETCH solo puede asignar o cambiar valores de variables locales, no recuperar ni cambiar datos de la base de datos

¿Qué se debe evitar en las funciones de instrucciones múltiples, aunque se permite?

  • Debe evitar escenarios en los que utilice columnas calculadas con funciones escalares; esto provocará reconstrucciones de índices y actualizaciones lentas que requerirán recálculos
  • Considere que cualquier función de instrucciones múltiples tiene su plan de ejecución y su impacto en el rendimiento
  • UDF con valores de tabla de declaraciones múltiples, si se usa en una expresión SQL o una declaración de combinación será lento debido al plan de ejecución no óptimo
  • No use funciones escalares en declaraciones WHERE y cláusulas ON a menos que esté seguro de que consultará un conjunto de datos pequeño, y ese conjunto de datos seguirá siendo pequeño en el futuro

Nombres de funciones y parámetros

Como cualquier otro nombre de objeto, los nombres de funciones deben cumplir con las reglas de los identificadores y deben ser únicos dentro de su esquema. Si está creando funciones escalares, puede ejecutarlas mediante la instrucción EXECUTE. En este caso, no tiene que poner el nombre del esquema en el nombre de la función. Vea el ejemplo de la llamada a la función EJECUTAR a continuación (creamos una función que devuelve la ocurrencia del enésimo día en un mes y luego recupera estos datos):

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 valores predeterminados para los parámetros de función, deben tener el prefijo "@" y cumplir con las reglas de nomenclatura de identificadores. Los parámetros solo pueden ser valores constantes, no se pueden usar en consultas SQL en lugar de tablas, vistas, columnas u otros objetos de la base de datos, y los valores no pueden ser expresiones, ni siquiera deterministas. Se permiten todos los tipos de datos, excepto el tipo de datos TIMESTAMP, y no se pueden usar tipos de datos no escalares, excepto los parámetros con valores de tabla. En las llamadas de función "estándar", debe especificar el atributo DEFAULT si desea dar al usuario final la capacidad de hacer que un parámetro sea opcional. En las nuevas versiones, utilizando la sintaxis EXECUTE, esto ya no es necesario, simplemente no ingresa este parámetro en la llamada de función. Si usamos tipos de tablas personalizados, deben marcarse como READONLY, lo que significa que no podemos cambiar el valor inicial dentro de la función, pero pueden usarse en cálculos y definiciones de otros parámetros.

Rendimiento de la función del servidor SQL

El último tema que cubriremos en este artículo, utilizando funciones del capítulo anterior, es el rendimiento de funciones. Extenderemos esta función y monitorearemos los tiempos de ejecución y la calidad de los planes de ejecución. Comenzamos creando otras versiones de funciones y continuamos con su comparación:

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

Cree algunas llamadas de prueba y casos de prueba

Empezamos con las versiones de la tabla:

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)

Creando datos de prueba:

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)

Prueba de rendimiento:

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

Comienzo del cronometraje:

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

Primero, no usamos ningún tipo de función para obtener una línea 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';

Ahora usamos una función con valores de tabla en línea 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 una función con valores de tabla en línea 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 no confiables, usamos una función escalar con enlace de esquema:

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'
 

A continuación, usamos una función escalar sin enlace 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'

Entonces, la función de tabla de sentencias múltiples derivó:

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'

Finalmente, la tabla de declaraciones múltiples se aplicó 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

Enumere todos los tiempos:

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 tabla anterior muestra claramente que debe considerar el rendimiento frente a la funcionalidad cuando utiliza funciones definidas por el usuario.

Conclusión

A muchos desarrolladores les gustan las funciones, principalmente porque son "construcciones lógicas". Puede crear fácilmente casos de prueba, son deterministas y encapsulantes, se integran muy bien con el flujo de código SQL y permiten flexibilidad en la parametrización. Son una buena opción cuando necesita implementar una lógica compleja que debe realizarse en un conjunto de datos más pequeño o ya filtrado que deberá reutilizar en múltiples escenarios. Las vistas de tabla en línea se pueden usar en vistas que necesitan parámetros, especialmente de las capas superiores (aplicaciones orientadas al cliente). Por otro lado, las funciones escalares son excelentes para trabajar con XML u otros formatos jerárquicos, ya que se pueden llamar recursivamente.

Las funciones de instrucciones múltiples definidas por el usuario son una gran adición a su pila de herramientas de desarrollo, pero debe comprender cómo funcionan y cuáles son sus limitaciones y desafíos de rendimiento. Su uso incorrecto puede destruir el rendimiento de cualquier base de datos, pero si sabe cómo usar estas funciones, pueden traer muchos beneficios para la reutilización y encapsulación de código.