Se superó el nivel de anidamiento de la función escalar autorreferenciada al agregar una selección

24

Propósito

Al intentar crear un ejemplo de prueba de una función de autorreferencia, una versión falla mientras que otra tiene éxito.

La única diferencia es un agregado SELECTal cuerpo de la función que resulta en un plan de ejecución diferente para ambos.


La función 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;

Llamando a la función

SELECT dbo.test5(3);

Devoluciones

(No column name)
3

La función que no 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;

Llamando a la función

SELECT dbo.test6(3);

o

SELECT dbo.test6(2);

Resultados en el error

Se excedió el máximo nivel almacenado de procedimiento, función, desencadenador o vista (límite 32).

Adivinando la causa

Hay un escalar de cálculo adicional en el plan estimado de la función fallida, que llama

<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">

Y expr1000 siendo

<ColumnReference Column="Expr1000" />
<ScalarOperator ScalarString="[dbo].[test6]((1))+[dbo].[test6]((2))">

Lo que podría explicar las referencias recursivas superiores a 32.

La pregunta real

El agregado SELECThace que la función se llame a sí misma una y otra vez, lo que resulta en un bucle sin fin, pero ¿por qué agregar un SELECTresultado de este resultado?


información adicional

Planes estimados de ejecución

DB <> Fiddle

Build version:
14.0.3045.24

Probado en los niveles de compatibilidad 100 y 140

Randi Vertongen
fuente

Respuestas:

26

Este es un error en la normalización del proyecto , expuesto mediante el uso de una subconsulta dentro de una expresión de caso con una función no determinista.

Para explicarlo, debemos tener en cuenta dos cosas por adelantado:

  1. SQL Server no puede ejecutar subconsultas directamente, por lo que siempre se desenrollan o se convierten en una aplicación .
  2. La semántica de CASEes tal que una THENexpresión solo debe evaluarse si la WHENcláusula devuelve verdadero.

La subconsulta (trivial) introducida en el caso problemático da como resultado un operador de aplicación (unión de bucles anidados). Para cumplir con el segundo requisito, SQL Server inicialmente coloca la expresión dbo.test6(1) + dbo.test6(2)en el lado interno de la aplicación:

escalar de cálculo resaltado

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

... con la CASEsemántica honrada por un predicado de transferencia en la unión:

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

El lado interno del bucle solo se evalúa si la condición de transferencia se evalúa como falsa (significado @i = 3). Todo esto es correcto hasta ahora. El cálculo escalar que sigue a los bucles anidados también honra la CASEsemántica correctamente:

[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)

El problema es que la etapa de normalización del proyecto de la compilación de consultas ve que Expr1000no está correlacionada y determina que sería seguro ( narrador: no lo es ) moverlo fuera del ciclo:

proyecto movido

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

Esto rompe la semántica * implementadas por el paso a través de predicados, por lo que la función se evalúa cuando no debería ser, y una infinidad de resultados de bucle.

Deberías reportar este error. Una solución alternativa es evitar que la expresión se mueva fuera de la aplicación haciendo que esté correlacionada (es decir, incluida @ien la expresión), pero esto es un truco, por supuesto. Hay una manera de deshabilitar la normalización del proyecto, pero antes me han pedido que no la comparta públicamente, por lo que no lo haré.

Este problema no surge en SQL Server 2019 cuando la función escalar está en línea , porque la lógica de línea opera directamente en el árbol analizado (mucho antes de la normalización del proyecto). La lógica simple en la pregunta puede simplificarse mediante la lógica de alineación a la no recursiva:

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

... que devuelve 3.

Otra forma de ilustrar el problema central es:

-- 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;

Se reproduce en las últimas compilaciones de todas las versiones desde 2008 R2 hasta 2019 CTP 3.0.

Otro ejemplo (sin una función escalar) proporcionado por Martin Smith :

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

Esto tiene todos los elementos clave necesarios:

  • CASE(implementado internamente como ScaOp_IIF)
  • Una función no determinista ( CRYPT_GEN_RANDOM)
  • Una subconsulta en la rama que no debe ejecutarse ( (SELECT ...))

* Estrictamente, la transformación anterior aún podría ser correcta si la evaluación de Expr1000se difiere correctamente, ya que solo se hace referencia por la construcción 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)

... pero esto requiere un indicador interno de ForceOrder (no una sugerencia de consulta), que tampoco está configurado. En cualquier caso, la implementación de la lógica aplicada por la normalización del proyecto es incorrecta o incompleta.

Informe de error en el sitio de comentarios de Azure para SQL Server.

Paul White dice GoFundMonica
fuente