web-dev-qa-db-fra.com

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

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

Plans d'exécution estimés

DB <> violon

Build version:
14.0.3045.24

Testé sur les niveaux de compatibilité 100 et 140

24
Randi Vertongen

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:

  1. SQL Server ne peut pas exécuter les sous-requêtes directement, elles sont donc toujours déroulées ou converties en appliquer.
  2. La sémantique de 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:

highlighted compute scalar

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

moved project

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

26
Paul White 9