Livello di annidamento della funzione scalare autoreferenziale superato quando si aggiunge una selezione


24

Scopo

Quando si tenta di creare un esempio di test di una funzione di autoreferenziazione, una versione ha esito negativo mentre un'altra ha esito positivo.

L'unica differenza è un'aggiunta SELECTal corpo della funzione che risulta in un piano di esecuzione diverso per entrambi.


La funzione che funziona

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;

Chiamando la funzione

SELECT dbo.test5(3);

ritorna

(No column name)
3

La funzione che non funziona

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;

Chiamando la funzione

SELECT dbo.test6(3);

o

SELECT dbo.test6(2);

Risultati nell'errore

Superata la procedura memorizzata, la funzione, il trigger o il livello di annidamento della vista superati (limite 32).

Indovinare la causa

Esiste un ulteriore scalare di calcolo sul piano stimato della funzione non riuscita, che chiama

<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 essere expr1000

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

Il che potrebbe spiegare i riferimenti ricorsivi superiori a 32.

La vera domanda

L'aggiunta SELECTfa sì che la funzione si richiami ripetutamente, dando luogo a un ciclo infinito, ma perché aggiungere un SELECTrisultato?


informazioni addizionali

Piani di esecuzione previsti

DB <> Fiddle

Build version:
14.0.3045.24

Testato su livelli_compatibilità 100 e 140

Risposte:


26

Questo è un bug nella normalizzazione del progetto , esposto usando una sottoquery all'interno di un'espressione case con una funzione non deterministica.

Per spiegare, dobbiamo notare due cose in anticipo:

  1. SQL Server non può eseguire direttamente le subquery, quindi vengono sempre srotolate o convertite in un applicare .
  2. La semantica di CASEè tale che THENun'espressione dovrebbe essere valutata solo se la WHENclausola ritorna vera.

La subquery (banale) introdotta nel caso problematico si traduce quindi in un operatore di applicazione (join loop nidificati). Per soddisfare il secondo requisito, SQL Server inizialmente posiziona l'espressione dbo.test6(1) + dbo.test6(2)sul lato interno dell'applicazione:

scalare di calcolo evidenziato

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

... con la CASEsemantica onorata da un predicato pass-through sul join:

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

Il lato interno del loop viene valutato solo se la condizione pass-through viene valutata come falsa (significato @i = 3). Questo è tutto corretto finora. Anche lo scalare di calcolo che segue l'unione di cicli nidificati onora CASEcorrettamente la semantica:

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

Il problema è che la fase di normalizzazione del progetto della compilazione delle query vede che Expr1000non è correlata e determina che sarebbe sicuro ( narratore: non lo è ) spostarlo al di fuori del ciclo:

progetto spostato

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

Ciò interrompe * la semantica implementata dal predicato pass-through , quindi la funzione viene valutata quando non dovrebbe essere e si ottiene un ciclo infinito.

Dovresti segnalare questo errore. Una soluzione alternativa consiste nel impedire che l'espressione venga spostata all'esterno dell'applicazione applicandola correlando (ovvero includendola @inell'espressione) ma questo è ovviamente un trucco. C'è un modo per disabilitare la normalizzazione del progetto, ma prima mi è stato chiesto di non condividerlo pubblicamente, quindi non lo farò.

Questo problema non si verifica in SQL Server 2019 quando la funzione scalare è inline , poiché la logica inlining opera direttamente sull'albero analizzato (ben prima della normalizzazione del progetto). La semplice logica nella domanda può essere semplificata dalla logica inline a quella non ricorsiva:

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

... che restituisce 3.

Un altro modo per illustrare il problema principale è:

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

Riproduce sugli ultimi build di tutte le versioni dal 2008 R2 al 2019 CTP 3.0.

Un ulteriore esempio (senza funzione scalare) fornito da Martin Smith :

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

Questo ha tutti gli elementi chiave necessari:

  • CASE(implementato internamente come ScaOp_IIF)
  • Una funzione non deterministica ( CRYPT_GEN_RANDOM)
  • Una sottoquery sul ramo che non deve essere eseguita ( (SELECT ...))

* Rigorosamente, la trasformazione di cui sopra potrebbe ancora essere corretta se la valutazione di Expr1000fosse rinviata correttamente, poiché è referenziata solo dalla costruzione sicura:

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

... ma questo richiede un flag ForceOrder interno (non un suggerimento per le query), che non è nemmeno impostato. In ogni caso, l'implementazione della logica applicata dalla normalizzazione del progetto è errata o incompleta.

Segnalazione di bug nel sito di feedback di Azure per SQL Server.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.