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:
- SQL Server non può eseguire direttamente le subquery, quindi vengono sempre srotolate o convertite in un applicare .
- La semantica di
CASE
è tale che THEN
un'espressione dovrebbe essere valutata solo se la WHEN
clausola 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:
[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))
... con la CASE
semantica 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 CASE
correttamente 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 Expr1000
non è correlata e determina che sarebbe sicuro ( narratore: non lo è ) spostarlo al di fuori del ciclo:
[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 @i
nell'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 Expr1000
fosse 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.