Le niveau d'imbrication des fonctions scalaires auto-référencées a été dépassé lors de l'ajout d'une sélection

24

Objectif

Lorsque vous essayez de créer un exemple de test d'une fonction d'auto-référencement, une version échoue tandis qu'une autre réussit.

La seule différence étant un ajout SELECTau corps de la fonction résultant en un plan d'exécution différent pour les deux.


La fonction qui fonctionne

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;

Appel de la fonction

SELECT dbo.test5(3);

Résultats

(No column name)
3

La fonction qui ne fonctionne pas

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;

Appel de la fonction

SELECT dbo.test6(3);

ou

SELECT dbo.test6(2);

Résultats dans l'erreur

Le niveau maximum d'imbrication de procédures, fonctions, déclencheurs ou vues stockées a été dépassé (limite 32).

Deviner la cause

Il existe un scalaire de calcul supplémentaire sur le plan estimé de la fonction défaillante, appelant

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

Et expr1000 étant

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

Ce qui pourrait expliquer les références récursives dépassant 32.

La vraie question

L'addition SELECTfait que la fonction s'appelle elle-même encore et encore, résultant en une boucle sans fin, mais pourquoi l'ajout d'un SELECTdonne ce résultat?


information additionnelle

Plans d'exécution estimés

DB <> Fiddle

Build version:
14.0.3045.24

Testé sur les niveaux de compatibilité 100 et 140

Randi Vertongen
la source

Réponses:

26

Il s'agit d'un bogue dans la normalisation de projet , exposé en utilisant une sous-requête à l'intérieur d'une expression de cas avec une fonction non déterministe.

Pour expliquer, nous devons noter deux choses à l'avance:

  1. SQL Server ne peut pas exécuter les sous-requêtes directement, elles sont donc toujours déroulées ou converties en application .
  2. La sémantique de CASEest telle qu'une THENexpression ne doit être évaluée que si la WHENclause renvoie true.

La sous-requête (triviale) introduite dans le cas problématique se traduit donc par un opérateur d'application (jointure de boucles imbriquées). Pour répondre à la deuxième exigence, SQL Server place initialement l'expression dbo.test6(1) + dbo.test6(2)sur le côté intérieur de l'application:

scalaire de calcul en surbrillance

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

... avec la CASEsémantique honorées par une répercussion prédicat sur la jointure:

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

Le côté intérieur de la boucle n'est évalué que si la condition de transmission est évaluée à false (signification @i = 3). Tout cela est correct jusqu'à présent. Le calcul scalaire suivant la jointure des boucles imbriquées honore également CASEcorrectement la sémantique:

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

Le problème est que l' étape de normalisation du projet de la compilation des requêtes voit que ce Expr1000n'est pas corrélé et détermine qu'il serait sûr ( narrateur: ce n'est pas ) de le déplacer en dehors de la boucle:

projet déplacé

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

Ce pauses * la sémantique mis en œuvre par le pass-through prédicat, de sorte que la fonction est évaluée quand il ne doit pas être, et un résultat de boucle infinie.

Vous devez signaler ce bogue. Une solution de contournement consiste à empêcher l'expression d'être déplacée en dehors de l'application en la rendant corrélée (c'est-à-dire en l'incluant @idans l'expression), mais il s'agit bien sûr d'un hack. Il existe un moyen de désactiver la normalisation du projet, mais on m'a déjà demandé de ne pas le partager publiquement, donc je ne le ferai pas.

Ce problème ne se pose pas dans SQL Server 2019 lorsque la fonction scalaire est insérée , car la logique d'inline fonctionne directement sur l'arborescence analysée (bien avant la normalisation du projet). La logique simple dans la question peut être simplifiée par la logique en ligne au non récursif:

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

... qui renvoie 3.

Une autre façon d'illustrer le problème central est:

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

Reproduit sur les dernières versions de toutes les versions de 2008 R2 à 2019 CTP 3.0.

Un autre exemple (sans fonction scalaire) fourni par Martin Smith :

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

Cela a tous les éléments clés nécessaires:

  • CASE(implémenté en interne comme ScaOp_IIF)
  • Une fonction non déterministe ( CRYPT_GEN_RANDOM)
  • Une sous-requête sur la branche qui ne doit pas être exécutée ( (SELECT ...))

* Strictement, la transformation ci-dessus pourrait toujours être correcte si l'évaluation de Expr1000était correctement différée, car elle n'est référencée que par la construction sûre:

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

... mais cela nécessite un indicateur ForceOrder interne (pas un indice de requête), qui n'est pas non plus défini. Dans tous les cas, l'implémentation de la logique appliquée par la normalisation du projet est incorrecte ou incomplète.

Rapport de bogue sur le site Azure Feedback pour SQL Server.

Paul White dit GoFundMonica
la source