Nível de aninhamento de função escalar auto-referenciado excedido ao adicionar um select


24

Finalidade

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 SELECTao 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 aninhado 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 questão real

O adicionado SELECTfaz com que a função seja chamada repetidamente, resultando em um loop infinito, mas por que adicionar um SELECTresultado a esse resultado?


informação adicional

Planos de execução estimados

DB <> Violino

Build version:
14.0.3045.24

Testado nos níveis de compatibilidade 100 e 140

Respostas:


26

Este é um erro na normalização do projeto , exposto usando uma subconsulta dentro de uma expressão de caso com uma função não determinística.

Para explicar, precisamos observar duas coisas com antecedência:

  1. O SQL Server não pode executar subconsultas diretamente, portanto elas sempre são desenroladas ou convertidas em um aplicativo .
  2. A semântica de CASEé tal que uma THENexpressão só deve ser avaliada se a WHENcláusula retornar verdadeira.

A subconsulta (trivial) introduzida no caso problemático 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:

escalar computacional destacado

[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))

... com a CASEsemântica respeitada por um predicado de passagem na junção:

[@i]=(1) OR [@i]=(2) OR IsFalseOrNull [@i]=(3)

O lado interno do loop é avaliado apenas se a condição de passagem for avaliada como falsa (significado @i = 3). Tudo está correto até agora. O Escalar de computação após a junção de loops aninhados também honra a CASEsemântica corretamente:

[Expr1001] = Scalar Operator(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)

O problema é que o estágio de normalização do projeto da compilação de consultas vê isso sem Expr1000correlação e determina que seria seguro ( narrador: não é ) movê-lo para fora do loop:

projeto movido

[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))

Isso quebra * a semântica implementada pelo predicado de passagem , de modo que a função é avaliada quando não deveria ser e resulta em um loop infinito.

Você deve relatar esse bug. Uma solução alternativa é impedir que a expressão seja movida para fora da aplicação, tornando-a correlacionada (isto é, incluindo @ia expressão), mas isso é óbvio. Existe uma maneira de desativar a normalização do projeto, mas me pediram antes para não compartilhá-lo publicamente, por isso não vou.

Esse problema não surge no SQL Server 2019 quando a função escalar é incorporada , porque a lógica embutida opera diretamente na árvore analisada (muito antes da normalização do projeto). A lógica simples na pergunta pode ser simplificada pela lógica embutida para o não recursivo:

[Expr1019] = (Scalar Operator((1)))
[Expr1045] = Scalar Operator(CONVERT_IMPLICIT(int,CONVERT_IMPLICIT(int,[Expr1019],0)+(2),0))

... que retorna 3.

Outra maneira de ilustrar a questão principal é:

-- Not schema bound to make it non-det
CREATE OR ALTER FUNCTION dbo.Error() 
RETURNS integer 
-- WITH INLINE = OFF -- SQL Server 2019 only
AS
BEGIN
    RETURN 1/0;
END;
GO
DECLARE @i integer = 1;

SELECT
    CASE 
        WHEN @i = 1 THEN 1
        WHEN @i = 2 THEN 2
        WHEN @i = 3 THEN (SELECT dbo.Error()) -- 'subquery'
        ELSE NULL
    END;

Reproduz nas versõ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 :

SELECT IIF(@@TRANCOUNT >= 0, 1, (SELECT CRYPT_GEN_RANDOM(4)/ 0))

Este possui todos os elementos-chave necessários:

  • CASE (implementado internamente como ScaOp_IIF )
  • Uma função não determinística (CRYPT_GEN_RANDOM )
  • Uma subconsulta na ramificação que não deve ser executada ( (SELECT ...))

* Estritamente, a transformação acima ainda pode estar correta se a avaliação de Expr1000for adiada corretamente, pois é referenciada apenas pela construção segura:

[Expr1002] = Scalar Operator(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)

... mas isso requer um sinalizador ForceOrder interno (não dica de consulta), que também não está definido. De qualquer forma, a implementação da lógica aplicada pela normalização do projeto está incorreta ou incompleta.

Relatório de bug no site de Feedback do Azure para SQL Server.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.