Por fim, não é possível forçar o SQL Server a avaliar uma UDF escalar apenas uma vez em uma consulta. No entanto, existem algumas medidas que podem ser tomadas para incentivá-lo. Com os testes, acredito que você pode obter algo que funcione com a versão atual do SQL Server, mas é possível que alterações futuras exijam a revisão do seu código.
Se é possível editar o código, uma boa primeira coisa a tentar é tornar a função determinística, se possível. Paul White aponta aqui que a função deve ser criada com a SCHEMABINDING
opção e o próprio código da função deve ser determinístico.
Depois de fazer a seguinte alteração:
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;
A consulta da pergunta é executada em 64 ms:
SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();
O plano de consulta não possui mais o operador de filtro:
Para garantir que ele seja executado apenas uma vez, podemos usar a nova DMV sys.dm_exec_function_stats lançada no SQL Server 2016:
SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');
Emitir um ALTER
contra a função redefinirá o execution_count
para esse objeto. A consulta acima retorna 1, o que significa que a função foi executada apenas uma vez.
Observe que o fato de a função ser determinística não significa que ela será avaliada apenas uma vez para qualquer consulta. De fato, para algumas consultas, a adição SCHEMABINDING
pode prejudicar o desempenho. Considere a seguinte 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;
O supérfluo DISTINCT
foi adicionado para se livrar de um operador de filtro. O plano parece promissor:
Com base nisso, seria de esperar que o UDF fosse avaliado uma vez e fosse usado como tabela externa na junção de loop aninhado. No entanto, a consulta leva 6446 ms para executar na minha máquina. De acordo com sys.dm_exec_function_stats
a função foi executada 100 vezes. Como isso é possível? Em " Escalares de computação, expressões e desempenho do plano de execução ", Paul White destaca que o operador Escalar de computação pode ser adiado:
Na maioria das vezes, um Escalar de computação simplesmente define uma expressão; o cálculo real é adiado até que algo mais tarde no plano de execução precise do resultado.
Para esta consulta, parece que a chamada UDF foi adiada até ser necessária, quando foi avaliada 100 vezes.
Curiosamente, o exemplo CTE é executado em 71 ms na minha máquina quando o UDF não está definido com SCHEMABINDING
, como na pergunta original. A função é executada apenas uma vez quando a consulta é executada. Aqui está o plano de consulta para isso:
Não está claro por que o Escalar de computação não é adiado. Pode ser porque o não determinismo da função limita a reorganização dos operadores que o otimizador de consulta pode fazer.
Uma abordagem alternativa é adicionar uma tabela pequena ao CTE e consultar a única linha nessa tabela. Qualquer tabela pequena serve, mas vamos usar o seguinte:
CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);
INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);
A consulta se torna:
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;
A adição do dbo.X_ONE_ROW_TABLE
adiciona incerteza para o otimizador. Se a tabela tiver zero linhas, o CTE retornará 0 linhas. Em qualquer caso, o otimizador não pode garantir que o CTE retornará uma linha se o UDF não for determinístico; portanto, parece provável que o UDF seja avaliado antes da junção. Eu esperaria que o otimizador varrasse dbo.X_ONE_ROW_TABLE
, use um agregado de fluxo para obter o valor máximo da linha retornada (que requer que a função seja avaliada) e use isso como a tabela externa para um loop aninhado dbo.X_100_INTEGERS
na consulta principal . Parece ser o que acontece :
A consulta é executada em cerca de 110 ms na minha máquina e o UDF é avaliado apenas uma vez de acordo com sys.dm_exec_function_stats
. Seria incorreto dizer que o otimizador de consulta é forçado a avaliar o UDF apenas uma vez. No entanto, é difícil imaginar uma reescrita do otimizador que levaria a uma consulta de custo mais baixo, mesmo com as limitações relacionadas ao UDF e ao cálculo do custo escalar.
Em resumo, para funções determinísticas (que devem incluir a SCHEMABINDING
opção), tente escrever a consulta da maneira mais simples possível. Se no SQL Server 2016 ou em uma versão posterior, confirme se a função foi executada apenas uma vez usando sys.dm_exec_function_stats
. Os planos de execução podem ser enganosos nesse sentido.
Para que as funções não consideradas determinantes pelo SQL Server sejam determinísticas, incluindo qualquer coisa que não possua a SCHEMABINDING
opção, uma abordagem é colocar o UDF em uma tabela CTE ou derivada cuidadosamente criada. Isso requer um pouco de cuidado, mas o mesmo CTE pode funcionar para funções determinísticas e não determinísticas.