Las UDF escalares siempre han sido un arma de doble filo:son excelentes para los desarrolladores, que pueden abstraer la lógica tediosa en lugar de repetirla en todas sus consultas, pero son horribles para el rendimiento del tiempo de ejecución en producción, porque el optimizador no t manejarlos muy bien. Esencialmente, lo que sucede es que las ejecuciones de UDF se mantienen separadas del resto del plan de ejecución, por lo que se las llama una vez por cada fila y no se pueden optimizar en función del número estimado o real de filas ni incorporarse al resto del plan.
Dado que, a pesar de nuestros mejores esfuerzos desde SQL Server 2000, no podemos detener de manera efectiva el uso de UDF escalares, ¿no sería genial hacer que SQL Server simplemente los maneje mejor?
SQL Server 2019 presenta una nueva característica llamada Scalar UDF Inlining. En lugar de mantener la función separada, se incorpora al plan general. Esto conduce a un plan de ejecución mucho mejor y, a su vez, a un mejor rendimiento del tiempo de ejecución.
Pero primero, para ilustrar mejor el origen del problema, comencemos con un par de tablas simples con solo unas pocas filas, en una base de datos que se ejecuta en SQL Server 2017 (o en 2019 pero con un nivel de compatibilidad más bajo):
CREATE DATABASE Whatever; GO ALTER DATABASE Whatever SET COMPATIBILITY_LEVEL = 140; GO USE Whatever; GO CREATE TABLE dbo.Languages ( LanguageID int PRIMARY KEY, Name sysname ); CREATE TABLE dbo.Employees ( EmployeeID int PRIMARY KEY, LanguageID int NOT NULL FOREIGN KEY REFERENCES dbo.Languages(LanguageID) ); INSERT dbo.Languages(LanguageID, Name) VALUES(1033, N'English'), (45555, N'Klingon'); INSERT dbo.Employees(EmployeeID, LanguageID) SELECT [object_id], CASE ABS([object_id]%2) WHEN 1 THEN 1033 ELSE 45555 END FROM sys.all_objects;
Ahora, tenemos una consulta simple en la que queremos mostrar a cada empleado y el nombre de su idioma principal. Digamos que esta consulta se usa en muchos lugares y/o de diferentes maneras, por lo que, en lugar de crear una unión en la consulta, escribimos una UDF escalar para abstraer esa unión:
CREATE FUNCTION dbo.GetLanguage(@id int) RETURNS sysname AS BEGIN RETURN (SELECT Name FROM dbo.Languages WHERE LanguageID = @id); END
Entonces nuestra consulta real se parece a esto:
SELECT TOP (6) EmployeeID, Language = dbo.GetLanguage(LanguageID) FROM dbo.Employees;
Si observamos el plan de ejecución de la consulta, extrañamente falta algo:
Plan de ejecución que muestra acceso a Empleados pero no a Idiomas
¿Cómo se accede a la tabla Idiomas? Este plan parece muy eficiente porque, al igual que la función misma, abstrae parte de la complejidad involucrada. De hecho, este plan gráfico es idéntico a una consulta que solo asigna una constante o variable al Language
columna:
SELECT TOP (6) EmployeeID, Language = N'Sanskrit' FROM dbo.Employees;
Pero si ejecuta un seguimiento en la consulta original, verá que en realidad hay seis llamadas a la función (una para cada fila) además de la consulta principal, pero SQL Server no devuelve estos planes.
También puede verificar esto revisando sys.dm_exec_function_stats
, pero esto no es una garantía :
SELECT [function] = OBJECT_NAME([object_id]), execution_count FROM sys.dm_exec_function_stats WHERE object_name(object_id) IS NOT NULL;
function execution_count ----------- --------------- GetLanguage 6
SentryOne Plan Explorer mostrará las declaraciones si genera un plan real desde dentro del producto, pero solo podemos obtenerlas del seguimiento, y todavía no hay planes recopilados o mostrados para las llamadas de funciones individuales:
Declaraciones de seguimiento para invocaciones de UDF escalares individuales
Todo esto los hace muy difíciles de solucionar, porque tienes que ir a cazarlos, incluso cuando ya sabes que están allí. También puede hacer un verdadero lío de análisis de rendimiento si está comparando dos planes en función de cosas como los costos estimados, porque no solo los operadores relevantes se esconden del diagrama físico, sino que los costos tampoco se incorporan en ninguna parte del plan.
Avance rápido a SQL Server 2019
Después de todos estos años de comportamiento problemático y causas raíz oscuras, lograron que algunas funciones se puedan optimizar en el plan de ejecución general. Scalar UDF Inlining hace que los objetos a los que acceden sean visibles para la resolución de problemas *y* permite incorporarlos a la estrategia del plan de ejecución. Ahora las estimaciones de cardinalidad (basadas en estadísticas) permiten unir estrategias que simplemente no eran posibles cuando la función se llamaba una vez para cada fila.
Podemos usar el mismo ejemplo anterior, ya sea crear el mismo conjunto de objetos en una base de datos de SQL Server 2019 o eliminar el caché del plan y subir el nivel de compatibilidad a 150:
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO ALTER DATABASE Whatever SET COMPATIBILITY_LEVEL = 150; GO
Ahora, cuando volvamos a ejecutar nuestra consulta de seis filas:
SELECT TOP (6) EmployeeID, Language = dbo.GetLanguage(LanguageID) FROM dbo.Employees;
Conseguimos un plan que incluye la tabla de Idiomas y los costes asociados a su acceso:
Plan que incluye acceso a objetos referenciados dentro de UDF escalar
Aquí, el optimizador eligió una combinación de bucles anidados pero, en otras circunstancias, podría haber elegido una estrategia de combinación diferente, contemplar el paralelismo y haber sido esencialmente libre de cambiar completamente la forma del plan. No es probable que vea esto en una consulta que devuelve 6 filas y no es un problema de rendimiento de ninguna manera, pero a escalas más grandes podría hacerlo.
El plan refleja que la función no se llama por fila; aunque la búsqueda se ejecuta seis veces, puede ver que la función en sí ya no aparece en sys.dm_exec_function_stats
. Una desventaja que puede eliminar es que, si usa este DMV para determinar si una función se está usando activamente (como hacemos a menudo con los procedimientos e índices), ya no será confiable.
Advertencias
No todas las funciones escalares son alineables e, incluso cuando una función *es* alineable, no necesariamente estará alineada en todos los escenarios. Esto a menudo tiene que ver con la complejidad de la función, la complejidad de la consulta involucrada o la combinación de ambos. Puede verificar si una función es inlineable en sys.sql_modules
vista de catálogo:
SELECT OBJECT_NAME([object_id]), definition, is_inlineable FROM sys.sql_modules;
Y si, por cualquier motivo, no desea que una determinada función (o cualquier función en una base de datos) esté en línea, no tiene que depender del nivel de compatibilidad de la base de datos para controlar ese comportamiento. Nunca me ha gustado ese acoplamiento suelto, que es similar a cambiar de habitación para ver un programa de televisión diferente en lugar de simplemente cambiar de canal. Puede controlar esto a nivel de módulo usando la opción EN LÍNEA:
ALTER FUNCTION dbo.GetLanguage(@id int) RETURNS sysname WITH INLINE = OFF AS BEGIN RETURN (SELECT Name FROM dbo.Languages WHERE LanguageID = @id); END GO
Y puede controlar esto a nivel de la base de datos, pero separado del nivel de compatibilidad:
ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = OFF;
Aunque tendrías que tener un caso de uso bastante bueno para mover ese martillo, en mi humilde opinión.
Conclusión
Ahora, no estoy sugiriendo que pueda ir y abstraer cada parte de la lógica en una UDF escalar, y asumir que ahora SQL Server solo se encargará de todos los casos. Si tiene una base de datos con mucho uso de UDF escalar, debe descargar el último CTP de SQL Server 2019, restaurar una copia de seguridad de su base de datos allí y consultar el DMV para ver cuántas de esas funciones se podrán incorporar cuando llegue el momento. Podría ser un punto importante la próxima vez que esté discutiendo sobre una actualización, ya que básicamente recuperará todo ese rendimiento y recuperará el tiempo perdido en la resolución de problemas.
Mientras tanto, si sufre de rendimiento UDF escalar y no actualizará a SQL Server 2019 en el corto plazo, puede haber otras formas de ayudar a mitigar los problemas.
Nota:Escribí y puse en cola este artículo antes de darme cuenta de que ya había publicado un artículo diferente en otro lugar.