Propósito
Ao tentar criar um exemplo de teste de uma função de auto-referência, uma versão falha enquanto outra é bem-sucedida.
A única diferença é uma adição SELECT
ao corpo da função, resultando em um plano de execução diferente para ambos.
A função que funciona
CREATE FUNCTION dbo.test5(@i int)
RETURNS INT
AS
BEGIN
RETURN(
SELECT TOP 1
CASE
WHEN @i = 1 THEN 1
WHEN @i = 2 THEN 2
WHEN @i = 3 THEN dbo.test5(1) + dbo.test5(2)
END
)
END;
Chamando a função
SELECT dbo.test5(3);
Devoluções
(No column name)
3
A função que não funciona
CREATE FUNCTION dbo.test6(@i int)
RETURNS INT
AS
BEGIN
RETURN(
SELECT TOP 1
CASE
WHEN @i = 1 THEN 1
WHEN @i = 2 THEN 2
WHEN @i = 3 THEN (SELECT dbo.test6(1) + dbo.test6(2))
END
)END;
Chamando a função
SELECT dbo.test6(3);
ou
SELECT dbo.test6(2);
Resultados no erro
Nível máximo de procedimento armazenado, função, gatilho ou exibição de aninhamento excedido (limite 32).
Adivinhando a causa
Há um escalar de computação adicional no plano estimado da função com falha, chamando
<ColumnReference Column="Expr1002" />
<ScalarOperator ScalarString="CASE WHEN [@i]=(1) THEN (1) ELSE CASE WHEN [@i]=(2) THEN (2) ELSE CASE WHEN [@i]=(3) THEN [Expr1000] ELSE NULL END END END">
E expr1000 sendo
<ColumnReference Column="Expr1000" />
<ScalarOperator ScalarString="[dbo].[test6]((1))+[dbo].[test6]((2))">
O que poderia explicar as referências recursivas superiores a 32.
A pergunta real
O adicionado SELECT
faz com que a função se chame repetidamente, resultando em um loop infinito, mas por que adicionar um está SELECT
dando esse resultado?
informação adicional
Build version:
14.0.3045.24
Testado nos níveis de compatibilidade 100 e 140
Este é um bug na normalização do projeto , exposto usando uma subconsulta dentro de uma expressão case com uma função não determinística.
Para explicar, precisamos observar duas coisas antecipadamente:
CASE
é tal que umaTHEN
expressão só deve ser avaliada se aWHEN
cláusula retornar true.A subconsulta (trivial) introduzida no caso problemático, portanto, resulta em um operador de aplicação (junção de loops aninhados). Para atender ao segundo requisito, o SQL Server inicialmente coloca a expressão
dbo.test6(1) + dbo.test6(2)
no lado interno da aplicação:...com a
CASE
semântica honrada por um predicado de passagem na junção:O lado interno do loop só é avaliado se a condição de passagem for avaliada como false (significando
@i = 3
). Tudo isso está correto até agora. O Compute Scalar após a junção de loops aninhados também respeita aCASE
semântica corretamente:O problema é que o estágio de normalização do projeto de compilação da consulta vê que
Expr1000
não está correlacionado e determina que seria seguro ( narrador: não é ) movê-lo para fora do loop:Isso quebra* a semântica implementada pelo predicado de passagem , de modo que a função é avaliada quando não deveria e resulta em um loop infinito.
Você deve relatar este bug. Uma solução é evitar que a expressão seja movida para fora da aplicação, tornando-a correlacionada (ou seja, incluindo
@i
na expressão), mas isso é um hack, é claro. Existe uma maneira de desabilitar a normalização do projeto, mas me pediram antes para não compartilhá-la publicamente, então não vou.Esse problema não surge no SQL Server 2019 quando a função escalar é embutida , pois a lógica embutida opera diretamente na árvore analisada (bem antes da normalização do projeto). A lógica simples na questão pode ser simplificada pela lógica inlining para a não recursiva:
...que retorna 3.
Outra maneira de ilustrar a questão central é:
Reproduz nas compilações mais recentes de todas as versões de 2008 R2 a 2019 CTP 3.0.
Um outro exemplo (sem uma função escalar) fornecido por Martin Smith :
Isso tem todos os elementos-chave necessários:
CASE
(implementado internamente comoScaOp_IIF
)CRYPT_GEN_RANDOM
)(SELECT ...)
)*Estritamente, a transformação acima ainda poderia estar correta se a avaliação de
Expr1000
fosse adiada corretamente, uma vez que é referenciada apenas pela construção segura:...mas isso requer um sinalizador ForceOrder interno (não uma dica de consulta), que também não está definido. Em qualquer caso, a implementação da lógica aplicada pela normalização do projeto está incorreta ou incompleta.
Relatório de bugs no site de comentários do Azure para SQL Server.