Quand les prédicats SARGable peuvent-ils être insérés dans une table CTE ou dérivée?

15

Sac de sable

En travaillant sur des articles de blog de qualité supérieure®, je suis tombé sur un comportement d'optimiseur que je trouvais vraiment exaspérant et intéressant. Je n'ai pas immédiatement d'explication, du moins pas avec qui je suis satisfait, alors je le mets ici au cas où quelqu'un intelligent se présenterait.

Si vous souhaitez suivre, vous pouvez récupérer la version 2013 du vidage de données Stack Overflow ici . J'utilise la table Commentaires, avec un index supplémentaire dessus.

CREATE INDEX [ix_ennui] ON [dbo].[Comments] ( [UserId], [Score] DESC );

Query One

Lorsque j'interroge la table comme ça, j'obtiens un plan de requête étrange .

WITH x
    AS
     (
         SELECT   TOP 101
                  c.UserId, c.Text, c.Score
         FROM     dbo.Comments AS c
         ORDER BY c.Score DESC
     )
SELECT *
FROM   x
WHERE  x.Score >= 500;

DES NOISETTES

Le prédicat SARGable sur Score n'est pas poussé à l'intérieur du CTE. C'est dans un opérateur de filtre beaucoup plus tard dans le plan.

DES NOISETTES

Ce que je trouve étrange, car il se ORDER BYtrouve sur la même colonne que le filtre.

Requête deux

Si je change la requête, elle est poussée.

WITH x
    AS
     (
         SELECT   c.UserId, c.Text, c.Score
         FROM     dbo.Comments AS c
     )
SELECT TOP 101 *
FROM   x
WHERE  x.Score >= 500
ORDER BY x.Score DESC;

Le plan de requête change également et s'exécute beaucoup plus rapidement, sans déversement sur le disque. Ils produisent tous deux les mêmes résultats, avec le prédicat lors de l'analyse d'index non cluster.

DES NOISETTES

DES NOISETTES

Requête trois

C'est l'équivalent d'écrire la requête comme ceci:

SELECT   TOP 101
         c.UserId, c.Text, c.Score
FROM     dbo.Comments AS c
WHERE c.Score >= 500
ORDER BY c.Score DESC;

Query Four

L'utilisation d'une table dérivée obtient le même "mauvais" plan de requête que la requête CTE initiale

SELECT *
FROM   (   SELECT   TOP 101
                    c.UserId, c.Text, c.Score
           FROM     dbo.Comments AS c
           ORDER BY c.Score DESC ) AS x
WHERE x.Score >= 500;

Les choses deviennent encore plus étranges quand ...

Je modifie la requête pour ordonner les données en ordre croissant et le filtre en <=.

Pour ne pas allonger cette question, je vais tout assembler.

Requêtes

--Derived table
SELECT *
FROM   (   SELECT   TOP 101
                    c.UserId, c.Text, c.Score
           FROM     dbo.Comments AS c
           ORDER BY c.Score ASC ) AS x
WHERE x.Score <= 500;


--TOP inside CTE
WITH x
    AS
     (
         SELECT   TOP 101
                  c.UserId, c.Text, c.Score
         FROM     dbo.Comments AS c
         ORDER BY c.Score ASC
     )
SELECT *
FROM   x
WHERE  x.Score <= 500;


--Written normally
SELECT   TOP 101
         c.UserId, c.Text, c.Score
FROM     dbo.Comments AS c
WHERE c.Score <= 500
ORDER BY c.Score ASC;

--TOP outside CTE
WITH x
    AS
     (
         SELECT   c.UserId, c.Text, c.Score
         FROM     dbo.Comments AS c
     )
SELECT TOP 101 *
FROM   x
WHERE  x.Score <= 500
ORDER BY x.Score ASC;

Des plans

Planifier le lien .

DES NOISETTES

Notez qu'aucune de ces requêtes ne tire parti de l'index non cluster - la seule chose qui change ici est la position de l'opérateur de filtrage. En aucun cas, le prédicat n'est poussé vers l'accès à l'index.

Une question apparaît!

Y a-t-il une raison pour laquelle un prédicat SARGable peut être poussé dans certains scénarios et pas dans d'autres? Les différences au sein des requêtes triées par ordre décroissant sont intéressantes, mais les différences entre celles et celles qui sont ascendantes sont bizarres.

Pour toute personne intéressée, voici les plans avec seulement un index sur Score:

Erik Darling
la source

Réponses:

11

Il y a quelques problèmes en jeu ici.

Pousser les prédicats du passé TOP

L'optimiseur ne peut pas actuellement pousser un prédicat au-delà de a TOP, même dans les cas limités où il serait sûr de le faire *. Cette limitation tient compte du comportement de toutes les requêtes dans la question où le prédicat est dans une portée plus élevée que le TOP.

La solution consiste à effectuer la réécriture manuellement. Le problème fondamental est similaire au cas de pousser des prédicats au-delà d'une fonction de fenêtre , sauf qu'il n'y a pas de règle spécialisée correspondante comme SelOnSeqPrj.

Mon opinion personnelle est qu'une règle d'exploration comme celle-ci SelOnTopreste non implémentée parce que les gens ont délibérément écrit des requêtes TOPdans le but de fournir une sorte de «barrière d'optimisation».

* En général, cela signifie que le prédicat doit apparaître dans la ORDER BYclause associée à la TOP, et la direction de toute inégalité doit correspondre à la direction du tri. La transformation devrait également prendre en compte le comportement de tri des valeurs NULL dans SQL Server. Dans l'ensemble, les limites signifient probablement que cette transformation ne serait généralement pas suffisamment utile dans la pratique pour justifier les efforts d'exploration supplémentaires.

Problèmes de coût

Les plans d'exécution restants dans la question peuvent être expliqués comme des choix basés sur les coûts en raison de la distribution des valeurs dans la Scorecolonne (beaucoup plus de lignes <= 500 que> = 500) et de l'effet de l' objectif de ligne introduit par le TOP.

Par exemple, la requête:

--Written normally
SELECT TOP (101)
    c.UserId, 
    c.[Text],
    c.Score
FROM dbo.Comments AS c
WHERE
    c.Score <= 500
ORDER BY
    c.Score ASC;

... produit un plan avec un prédicat apparemment non poussé dans un filtre:

filtre tardif en raison de l'objectif de ligne

Notez que le tri est estimé produire 101 lignes. C'est l'effet de l'objectif de ligne ajouté par le Top. Cela affecte suffisamment le coût estimé du tri et du filtre pour donner l'impression que c'est l'option la moins chère. Le coût estimé de ce plan est de 2401,39 unités.

Si nous désactivons les objectifs de ligne avec un indice de requête:

--Written normally
SELECT TOP (101)
    c.UserId, 
    c.[Text],
    c.Score
FROM dbo.Comments AS c
WHERE
    c.Score <= 500
ORDER BY
    c.Score ASC
OPTION (USE HINT ('DISABLE_OPTIMIZER_ROWGOAL'));

... le plan d'exécution produit est le suivant:

plan sans objectif de ligne

Le prédicat a été inséré dans l'analyse en tant que prédicat résiduel non négociable, et le coût de l'ensemble du plan est de 2402,32 unités.

Notez que le <= 500prédicat ne devrait pas filtrer les lignes. Si vous aviez choisi un nombre plus petit, par exemple <= 50, l'optimiseur aurait préféré le plan de prédicat poussé indépendamment de l'effet d'objectif de ligne.

Pour la requête avec Score DESCet un Score >= 500prédicat:

--Written normally
SELECT TOP (101)
    c.UserId, 
    c.[Text],
    c.Score
FROM dbo.Comments AS c
WHERE
    c.Score >= 500
ORDER BY
    c.Score DESC;

Maintenant, le prédicat devrait être très sélectif, donc l'optimiseur choisit de pousser le prédicat et d' utiliser l'index non cluster avec des recherches:

prédicat sélectif

Encore une fois, l'optimiseur a considéré plusieurs alternatives et l'a choisie comme l'option apparemment la moins chère, comme d'habitude.

Paul White 9
la source