En última instancia, no es posible forzar a SQL Server a evaluar un UDF escalar solo una vez en una consulta. Sin embargo, hay algunos pasos que se pueden tomar para alentarlo. Con las pruebas, creo que puede obtener algo que funcione con la versión actual de SQL Server, pero es posible que los cambios futuros requieran que vuelva a visitar su código.
Si es posible editar el código, lo primero que debe intentar es hacer que la función sea determinista si es posible. Paul White señala aquí que la función debe crearse con la SCHEMABINDING
opción y el código de la función en sí debe ser determinista.
Después de hacer el siguiente cambio:
CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
DECLARE @tbl TABLE (VAL VARCHAR(5));
-- make the function expensive to call
INSERT INTO @tbl
SELECT [VALUE]
FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');
RETURN 1;
END;
La consulta de la pregunta se ejecuta en 64 ms:
SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();
El plan de consulta ya no tiene el operador de filtro:
Para asegurarnos de que se ejecutó solo una vez, podemos usar el nuevo DMV sys.dm_exec_function_stats lanzado en SQL Server 2016:
SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');
Emitir un en ALTER
contra de la función restablecerá execution_count
para ese objeto. La consulta anterior devuelve 1, lo que significa que la función solo se ejecutó una vez.
Tenga en cuenta que el hecho de que la función sea determinista no significa que se evaluará solo una vez para cualquier consulta. De hecho, para algunas consultas, agregar SCHEMABINDING
puede degradar el rendimiento. Considere la siguiente consulta:
WITH cte (UDF_VALUE) AS
(
SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;
Lo superfluo DISTINCT
se agregó para deshacerse de un operador de filtro. El plan parece prometedor:
Basado en eso, uno esperaría que la UDF se evalúe una vez y se use como la tabla externa en la unión de bucle anidado. Sin embargo, la consulta tarda 6446 ms en ejecutarse en mi máquina. Según sys.dm_exec_function_stats
la función se ejecutó 100 veces. ¿Cómo es eso posible? En " Compute Scalars, Expressions and Execution Plan Performance ", Paul White señala que el operador Compute Scalar puede diferirse:
La mayoría de las veces, un Escalar Compute simplemente define una expresión; el cálculo real se difiere hasta que algo posterior en el plan de ejecución necesite el resultado.
Para esta consulta, parece que la llamada UDF se aplazó hasta que se necesitó, momento en el que se evaluó 100 veces.
Curiosamente, el ejemplo de CTE se ejecuta en 71 ms en mi máquina cuando el UDF no está definido con SCHEMABINDING
, como en la pregunta original. La función solo se ejecuta una vez cuando se ejecuta la consulta. Aquí está el plan de consulta para eso:
No está claro por qué no se aplaza el cálculo escalar. Podría deberse a que el no determinismo de la función limita la reorganización de los operadores que puede hacer el optimizador de consultas.
Un enfoque alternativo es agregar una tabla pequeña al CTE y consultar la única fila en esa tabla. Cualquier tabla pequeña servirá, pero usemos lo siguiente:
CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);
INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);
La consulta se convierte en:
WITH cte (UDF_VALUE) AS
(
SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
FROM dbo.X_ONE_ROW_TABLE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;
La adición de dbo.X_ONE_ROW_TABLE
agrega incertidumbre para el optimizador. Si la tabla tiene cero filas, el CTE devolverá 0 filas. En cualquier caso, el optimizador no puede garantizar que el CTE devolverá una fila si el UDF no es determinista, por lo que parece probable que el UDF sea evaluado antes de la unión. Esperaría que el optimizador escanee dbo.X_ONE_ROW_TABLE
, use un agregado de flujo para obtener el valor máximo de la fila devuelta (lo que requiere que se evalúe la función), y usar eso como la tabla externa para una unión de bucle anidado dbo.X_100_INTEGERS
en la consulta principal . Esto parece ser lo que sucede :
La consulta se ejecuta en aproximadamente 110 ms en mi máquina y el UDF se evalúa solo una vez de acuerdo con sys.dm_exec_function_stats
. Sería incorrecto decir que el optimizador de consultas se ve obligado a evaluar el UDF solo una vez. Sin embargo, es difícil imaginar una reescritura del optimizador que conduzca a una consulta de menor costo, incluso con las limitaciones en torno a UDF y calcular el costo escalar.
En resumen, para funciones deterministas (que deben incluir la SCHEMABINDING
opción) intente escribir la consulta de la manera más simple posible. Si está en SQL Server 2016 o una versión posterior, confirme que la función solo se ejecutó una vez usando sys.dm_exec_function_stats
. Los planes de ejecución pueden ser engañosos en ese sentido.
Para las funciones que SQL Server no considera deterministas, incluso cualquier cosa que no tenga la SCHEMABINDING
opción, un enfoque es colocar el UDF en un CTE cuidadosamente elaborado o en una tabla derivada. Esto requiere un poco de cuidado, pero el mismo CTE puede funcionar tanto para funciones deterministas como no deterministas.