Pourquoi la jonction d'élimination ne fonctionne-t-elle pas avec sys.query_store_plan?

10

Voici une simplification d'un problème de performances rencontré avec le magasin de requêtes:

CREATE TABLE #tears
(
    plan_id bigint NOT NULL
);

INSERT #tears (plan_id) 
VALUES (1);

SELECT
    T.plan_id
FROM #tears AS T
LEFT JOIN sys.query_store_plan AS QSP
    ON QSP.plan_id = T.plan_id;

La plan_idcolonne est documentée comme étant la clé primaire de sys.query_store_plan, mais le plan d'exécution n'utilise pas l' élimination des jointures comme prévu:

  1. Aucun attribut n'est projeté depuis le DMV.
  2. La clé primaire DMV plan_idne peut pas dupliquer des lignes de la table temporaire
  3. A LEFT JOINest utilisé, donc aucune ligne ne Tpeut être supprimée.

Plan d'exécution

graphique du plan

Pourquoi cela, et que peut-on faire pour obtenir l'élimination des jointures ici?

Paul White 9
la source

Réponses:

16

La documentation est un peu trompeuse. Le DMV est une vue non matérialisée et n'a pas de clé primaire en tant que telle. Les définitions sous-jacentes sont un peu complexes mais une définition simplifiée de sys.query_store_planest:

CREATE VIEW sys.query_store_plan AS
SELECT
    PPM.plan_id
    -- various other attributes
FROM sys.plan_persist_plan_merged AS PPM
LEFT JOIN sys.syspalvalues AS P
    ON P.class = 'PFT' 
    AND P.[value] = plan_forcing_type;

En outre, sys.plan_persist_plan_mergedest également une vue, bien qu'il soit nécessaire de se connecter via la connexion administrateur dédiée pour voir sa définition. Encore une fois, simplifié:

CREATE VIEW sys.plan_persist_plan_merged AS   
SELECT     
    P.plan_id as plan_id,    
    -- various other attributes
FROM sys.plan_persist_plan P 
    -- NOTE - in order to prevent potential deadlock
    -- between QDS_STATEMENT_STABILITY LOCK and index locks   
    WITH (NOLOCK) 
LEFT JOIN sys.plan_persist_plan_in_memory PM
    ON P.plan_id = PM.plan_id;

Les index sur sys.plan_persist_plansont:

╔════════════════════════╦════════════════════════ ══════════════╦═════════════╗
║ nom_index ║ description_index ║ clés_index ║
╠════════════════════════╬════════════════════════ ══════════════╬═════════════╣
║ plan_persist_plan_cidx ║ clusterisé, unique situé sur PRIMARY ║ plan_id ║
║ plan_persist_plan_idx1 ║ non cluster situé sur PRIMARY ║ query_id (-) ║
╚════════════════════════╩════════════════════════ ══════════════╩═════════════╝

Il plan_idest donc contraint d'être unique sur sys.plan_persist_plan.

Maintenant, sys.plan_persist_plan_in_memoryest une fonction de table en continu, présentant une vue tabulaire des données uniquement conservées dans les structures de mémoire interne. En tant que tel, il n'a pas de contraintes uniques.

Au fond, la requête en cours d'exécution est donc équivalente à:

DECLARE @t1 table (plan_id integer NOT NULL);
DECLARE @t2 table (plan_id integer NOT NULL UNIQUE CLUSTERED);
DECLARE @t3 table (plan_id integer NULL);

SELECT 
    T1.plan_id
FROM @t1 AS T1 
LEFT JOIN
(
    SELECT 
        T2.plan_id
    FROM @t2 AS T2
    LEFT JOIN @t3 AS T3 
        ON T3.plan_id = T2.plan_id
) AS Q1
    ON Q1.plan_id = T1.plan_id;

... qui ne produit pas d'élimination des jointures:

entrez la description de l'image ici

Pour aller directement au cœur du problème, le problème est la requête interne:

DECLARE @t2 table (plan_id integer NOT NULL UNIQUE CLUSTERED);
DECLARE @t3 table (plan_id integer NULL);

SELECT 
    T2.plan_id
FROM @t2 AS T2
LEFT JOIN @t3 AS T3 
    ON T3.plan_id = T2.plan_id;

... il est clair que la jointure gauche peut entraîner la @t2duplication des lignes car elle @t3n'a pas de contrainte d'unicité plan_id. Par conséquent, la jointure ne peut pas être éliminée:

entrez la description de l'image ici

Pour contourner ce problème, nous pouvons explicitement dire à l'optimiseur que nous n'avons pas besoin de plan_idvaleurs en double :

DECLARE @t2 table (plan_id integer NOT NULL UNIQUE CLUSTERED);
DECLARE @t3 table (plan_id integer NULL);

SELECT DISTINCT
    T2.plan_id
FROM @t2 AS T2
LEFT JOIN @t3 AS T3 
    ON T3.plan_id = T2.plan_id;

La jointure externe à @t3peut maintenant être éliminée:

entrez la description de l'image ici

Appliquer cela à la vraie requête:

SELECT DISTINCT
    T.plan_id
FROM #tears AS T
LEFT JOIN sys.query_store_plan AS QSP
    ON QSP.plan_id = T.plan_id;

De même, nous pourrions ajouter GROUP BY T.plan_idau lieu de DISTINCT. Quoi qu'il en soit, l'optimiseur peut désormais raisonner correctement sur l' plan_idattribut tout au long des vues imbriquées et éliminer les deux jointures externes comme vous le souhaitez:

entrez la description de l'image ici

Notez que rendre plan_idunique dans la table temporaire ne serait pas suffisant pour obtenir l'élimination des jointures, car cela n'empêcherait pas des résultats incorrects. Nous devons explicitement rejeter les plan_idvaleurs en double du résultat final pour permettre à l'optimiseur de travailler sa magie ici.

Paul White 9
la source