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

8

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 WHEREclause, SQL Server semble appliquer le CTE uniquement à cette valeur, donnant un plan de requête avec un nombre de lignes réel faible, et cetera :

plan de requête avec valeur littérale

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

plan de requête avec valeur de paramètre

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;
binki
la source

Réponses:

12

La réponse de Randi Vertongen 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 spool d'index paresseux , 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 en réponse à l' utilisation d'EXCEPT dans une expression de table commune récursive .

Vous souhaitez spécifier un prédicat (filtre) en dehors du CTE et demander à l'optimiseur de requête de pousser ce filtre vers le bas à l'intérieur de la récursivité (réécrit en itération) et de le faire appliquer au membre d'ancrage. Cela signifie que la récursivité commence uniquement avec 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 dans la partie d' ancrage 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'indice non documenté OPTION(QUERYRULEOFF SelOnIterator). Lorsqu'il est utilisé, l'optimiseur ne peut plus pousser les prédicats avec une valeur littérale vers le bas vers 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 pourrait également être fait pour travailler avec des variables ou des paramètres en spécifiant OPTION (RECOMPILE), car cette indication active l' optimisation d'intégration des paramètres , où 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 SelOnIteratorrègle a été améliorée pour fonctionner également avec des variables et des 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.

Paul White 9
la source
10

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:

  • En utilisant OPTION(RECOMPILE)ainsi le filtrage se produit 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êtes

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 @idactivé 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. AQ / A ce 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:

entrez la description de l'image ici

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

entrez la description de l'image ici

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

Randi Vertongen
la source