web-dev-qa-db-fra.com

Pourquoi ce CTE récursif avec un paramètre n'utilise-t-il pas un index quand il le fait avec un littéral?

J'utilise un CTE récursif sur une structure arborescente pour répertorier tous les descendants d'un nœud particulier dans l'arbre. Si j'écris une valeur de nœud littéral dans ma clause WHERE, SQL Server semble appliquer le CTE uniquement à cette valeur, donnant un plan de requête avec nombre de lignes réelles faibles, et cetera :

query plan with literal value

Cependant, si je passe la valeur en paramètre, il semble réaliser (spool) le CTE puis le filtrer après coup :

query plan with parameter value

Je pourrais mal lire les plans. Je n'ai pas remarqué de problème de performances, mais je crains que la réalisation du CTE ne cause des problèmes avec des ensembles de données plus volumineux, en particulier dans un système plus chargé. De plus, je compose normalement cette traversée sur elle-même: je traverse jusqu'aux ancêtres et redescends aux descendants (pour m'assurer de rassembler tous les nœuds liés). En raison de la façon dont mes données sont, chaque ensemble de nœuds "liés" est plutôt petit, donc la réalisation du CTE n'a pas de sens. Et lorsque SQL Server semble réaliser le CTE, il me donne des chiffres assez importants dans ses décomptes "réels".

Existe-t-il un moyen pour que la version paramétrée de la requête agisse comme la version littérale? Je veux mettre le CTE dans une vue réutilisable.

Requête avec littéral:

CREATE PROCEDURE #c AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = 24
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c;

Requête avec paramètre:

CREATE PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c 24;

Code de configuration:

DECLARE @count BIGINT = 100000;
CREATE TABLE #tree (
     Id BIGINT NOT NULL PRIMARY KEY
    ,ParentId BIGINT
);
CREATE INDEX tree_23lk4j23lk4j ON #tree (ParentId);
WITH number AS (SELECT
         CAST(1 AS BIGINT) Value
    UNION ALL SELECT
         n.Value * 2 + 1
    FROM number n
    WHERE n.Value * 2 + 1 <= @count
    UNION ALL SELECT
         n.Value * 2
    FROM number n
    WHERE n.Value * 2 <= @count)
INSERT #tree (Id, ParentId)
SELECT n.Value, CASE WHEN n.Value % 3 = 0 THEN n.Value / 4 END
FROM number n;
8
binki

Randi Vertongen réponse explique correctement comment obtenir le plan que vous souhaitez avec la version paramétrée de la requête. Cette réponse complète celle en abordant le titre de la question au cas où vous seriez intéressé par les détails.

SQL Server réécrit les expressions de table commune (CTE) récursives en tant qu'itération. Tout depuis le Lazy Index Spool vers le bas est l'implémentation d'exécution de la traduction itérative. J'ai écrit un compte rendu détaillé du fonctionnement de cette section d'un plan d'exécution dans réponse à tilisation de EXCEPT dans une expression de table commune récursive .

Vous voulez spécifier un prédicat (filtre) en dehors du CTE et avoir l'optimiseur de requête Poussez ce filtre vers le bas à l'intérieur de la récursivité (réécrite en itération) et faites-la appliquer au membre d'ancrage. Cela signifie que la récursivité commence avec uniquement les enregistrements qui correspondent à ParentId = @Id.

Il s'agit d'une attente tout à fait raisonnable, qu'une valeur littérale, une variable ou un paramètre soit utilisé; cependant, l'optimiseur ne peut faire que des choses pour lesquelles des règles ont été écrites. Les règles spécifient comment un arbre de requête logique est modifié pour réaliser une transformation particulière. Ils incluent une logique pour s'assurer que le résultat final est sûr - c'est-à-dire qu'il renvoie exactement les mêmes données que la spécification de requête d'origine dans tous les cas possibles.

La règle chargée de pousser les prédicats sur un CTE récursif est appelée SelOnIterator - une sélection relationnelle (= prédicat) sur un itérateur implémentant la récursivité. Plus précisément, cette règle peut copier une sélection vers le bas ancre de l'itération récursive:

Sel(Iter(A,R)) -> Sel(Iter(Sel(A),R))

Cette règle peut être désactivée avec l'indication non documentée OPTION(QUERYRULEOFF SelOnIterator). Lorsque cela est utilisé, l'optimiseur ne peut plus pousser les prédicats avec une valeur littérale jusqu'à l'ancre d'un CTE récursif. Vous ne voulez pas cela, mais cela illustre le point.

À l'origine, cette règle se limitait à travailler sur des prédicats avec des valeurs littérales uniquement. Il peut également être fait pour travailler avec des variables ou des paramètres en spécifiant OPTION (RECOMPILE), car cette indication active l'optimisation de l'incorporation de paramètres , grâce à quoi le La valeur littérale d'exécution de la variable (ou paramètre) est utilisée lors de la compilation du plan. Le plan n'est pas mis en cache, donc l'inconvénient est une nouvelle compilation à chaque exécution.

À un moment donné, la règle SelOnIterator a été améliorée pour fonctionner également avec les variables et les paramètres. Pour éviter les modifications de plan inattendues, cela a été protégé sous l'indicateur de trace 4199, le niveau de compatibilité de la base de données et le niveau de compatibilité du correctif de l'optimiseur de requête. Il s'agit d'un schéma tout à fait normal pour les améliorations de l'optimiseur, qui ne sont pas toujours documentées. Les améliorations sont normalement bonnes pour la plupart des gens, mais il y a toujours une chance que tout changement entraîne une régression pour quelqu'un.

Je veux mettre le CTE dans une vue réutilisable

Vous pouvez utiliser une fonction table inline au lieu d'une vue. Fournissez la valeur que vous souhaitez pousser en tant que paramètre et placez le prédicat dans le membre d'ancrage récursif.

Si vous préférez, l'activation de l'indicateur de trace 4199 globalement est également une option. De nombreux changements d'optimiseur sont couverts par cet indicateur, vous devez donc tester soigneusement votre charge de travail avec celui-ci activé et être prêt à gérer les régressions.

12
Paul White 9

Bien que pour le moment je n'ai pas le titre du correctif réel, le meilleur plan de requête sera utilisé lors de l'activation des correctifs de l'optimiseur de requête sur votre version (SQL Server 2012).

Quelques autres méthodes sont:

  • Utilisation de OPTION(RECOMPILE) pour que le filtrage ait lieu plus tôt, sur la valeur littérale.
  • Sur SQL Server 2016 ou version ultérieure, les correctifs avant cette version sont appliqués automatiquement et la requête doit également s'exécuter de manière équivalente au meilleur plan d'exécution.

Correctifs de l'optimiseur de requête

Vous pouvez activer ces correctifs avec

  • Traceflag 4199 avant SQL Server 2016
  • ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES=ON; À partir de SQL Server 2016. (non nécessaire pour votre correctif)

Le filtrage sur @id Est appliqué plus tôt aux membres récursifs et d'ancrage dans le plan d'exécution avec le correctif activé.

Le traceflag peut être ajouté au niveau de la requête:

OPTION(QUERYTRACEON 4199)

Lors de l'exécution de la requête sur SQL Server 2012 SP4 GDR ou SQL Server 2014 SP3 avec Traceflag 4199, le meilleur plan de requête est choisi:

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
    OPTION( QUERYTRACEON 4199 );

END;
GO
EXEC #c 24;

Plan de requête sur SQL Server 2014 SP3 avec traceflag 4199

Plan de requête sur SQL Server 2012 SP4 GDR avec traceflag 4199

Plan de requête sur SQL Server 2012 SP4 GDR sans traceflag 4199

Le consensus principal est d'activer globalement traceflag 4199 lors de l'utilisation d'une version antérieure à SQL Server 2016. Ensuite, il est possible de discuter de l'activation ou non. Un Q/A à ce sujet ici .


Niveau de compatibilité 130 ou 140

Lors du test de la requête paramétrée sur une base de données avec compatibility_level = 130 ou 140, le filtrage se produit plus tôt:

enter image description here

En raison du fait que les "anciens" correctifs de traceflag 4199 sont activés sur SQL Server 2016 et versions ultérieures.


OPTION (RECOMPILE)

Même si une procédure est utilisée, SQL Server pourra filtrer la valeur littérale lors de l'ajout de OPTION(RECOMPILE);.

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
OPTION(
RECOMPILE )

END;
GO

enter image description here

Plan de requête sur SQL Server 2012 SP4 GDR avec OPTION (RECOMPILE)

10
Randi Vertongen