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 SELECT
ajouté au 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);
Renvoie
(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);
entraîne l'erreur
Dépassement du nombre maximal de procédures, fonctions, déclencheurs ou vues d'imbrication stockées (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 question réelle
Le SELECT
ajouté fait que la fonction s'appelle elle-même encore et encore, résultant en une boucle sans fin, mais pourquoi l'ajout d'un SELECT
donne ce résultat?
Informations supplémentaires
Build version:
14.0.3045.24
Testé sur les niveaux de compatibilité 100 et 140
Il s'agit d'un bogue dans normalisation du 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:
CASE
est telle qu'une expression THEN
ne doit être évaluée que si la clause WHEN
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:
[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))
... avec la sémantique CASE
honorée par un prédicat pass-through sur la jointure:
[@i]=(1) OR [@i]=(2) OR IsFalseOrNull [@i]=(3)
L'intérieur de la boucle n'est évalué que si la condition pass-through est évaluée à false (ce qui signifie @i = 3
). Tout cela est correct jusqu'à présent. La Compute Scalar suivant la jointure des boucles imbriquées honore également correctement la sémantique CASE
:
[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 normalisation du projet de la compilation des requêtes voit que Expr1000
n'est pas corrélé et détermine qu'il serait sûr ( narrateur: ce n'est pas) de le déplacer hors de la boucle:
[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))
Cela brise * la sémantique implémentée par le prédicat pass-through, donc la fonction est évaluée quand elle ne devrait pas l'être, et une boucle infinie en résulte.
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 corrélant (c'est-à-dire en incluant @i
dans l'expression) mais c'est bien sûr 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 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
)CRYPT_GEN_RANDOM
)(SELECT ...)
)* Strictement, la transformation ci-dessus pourrait toujours être correcte si l'évaluation de Expr1000
a été différé correctement, car il n'est référencé 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 interne ForceOrder (pas un indice de requête), qui n'est pas défini non plus. Dans tous les cas, l'implémentation de la logique appliquée par normalisation du projet est incorrecte ou incomplète.
Rapport de bogue sur le site Azure Feedback pour SQL Server.