Pourquoi le coût estimé de (le même) 1000 recherches sur un indice unique diffère-t-il dans ces plans?

28

Dans les requêtes ci-dessous, les deux plans d'exécution devraient effectuer 1 000 recherches sur un index unique.

Les recherches sont motivées par un balayage ordonné sur la même table source, donc devraient apparemment finir par rechercher les mêmes valeurs dans le même ordre.

Les deux boucles imbriquées ont <NestedLoops Optimized="false" WithOrderedPrefetch="true">

Quelqu'un sait pourquoi cette tâche coûte 0,172434 dans le premier plan mais 3,01702 dans le second?

(La raison de la question est que la première requête m'a été suggérée comme une optimisation en raison du coût apparemment beaucoup plus bas du plan. Il me semble en fait que cela fait plus de travail, mais j'essaie simplement d'expliquer l'écart. .)

Installer

CREATE TABLE dbo.Target(KeyCol int PRIMARY KEY, OtherCol char(32) NOT NULL);

CREATE TABLE dbo.Staging(KeyCol int PRIMARY KEY, OtherCol char(32) NOT NULL); 

INSERT INTO dbo.Target
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY @@SPID), LEFT(NEWID(),32)
FROM master..spt_values v1,  
     master..spt_values v2;

INSERT INTO dbo.Staging
SELECT TOP (1000) ROW_NUMBER() OVER (ORDER BY @@SPID), LEFT(NEWID(),32)
FROM master..spt_values v1;

Lien Requête 1 «Coller le plan»

WITH T
     AS (SELECT *
         FROM   Target AS T
         WHERE  T.KeyCol IN (SELECT S.KeyCol
                             FROM   Staging AS S))
MERGE T
USING Staging S
ON ( T.KeyCol = S.KeyCol )
WHEN NOT MATCHED THEN
  INSERT ( KeyCol, OtherCol )
  VALUES(S.KeyCol, S.OtherCol )
WHEN MATCHED AND T.OtherCol > S.OtherCol THEN
  UPDATE SET T.OtherCol = S.OtherCol;

Lien Requête 2 «Coller le plan»

MERGE Target T
USING Staging S
ON ( T.KeyCol = S.KeyCol )
WHEN NOT MATCHED THEN
  INSERT ( KeyCol, OtherCol )
  VALUES( S.KeyCol, S.OtherCol )
WHEN MATCHED AND T.OtherCol > S.OtherCol THEN
  UPDATE SET T.OtherCol = S.OtherCol; 

Requête 1

Requête 2

Ce qui précède a été testé sur SQL Server 2014 (SP2) (KB3171021) - 12.0.5000.0 (X64)


@Joe Obbish souligne dans les commentaires qu'une repro plus simple serait

SELECT *
FROM staging AS S 
  LEFT OUTER JOIN Target AS T 
    ON T.KeyCol = S.KeyCol;

contre

SELECT *
FROM staging AS S 
  LEFT OUTER JOIN (SELECT * FROM Target) AS T 
    ON T.KeyCol = S.KeyCol;

Pour la table intermédiaire de 1 000 lignes, les deux ci-dessus ont toujours la même forme de plan avec des boucles imbriquées et le plan sans que la table dérivée n'apparaisse moins cher, mais pour une table intermédiaire de 10 000 lignes et la même table cible que ci-dessus, la différence de coûts change le plan forme (avec une analyse complète et une fusion fusionnée semblant relativement plus attrayante que les recherches coûteuses) montrant que cet écart de coût peut avoir des implications autres que simplement rendre la comparaison des plans plus difficile.

entrez la description de l'image ici

Martin Smith
la source

Réponses:

21

Quelqu'un sait pourquoi cette tâche coûte 0,172434 dans le premier plan mais 3,01702 dans le second?

De manière générale, une recherche de côté interne sous une jointure de boucles imbriquées est estimée en supposant un modèle d'E / S aléatoire. Il existe une réduction basée sur le remplacement simple pour les accès ultérieurs, tenant compte du risque que la page requise ait déjà été mise en mémoire par une itération précédente. Cette évaluation de base produit le coût standard (plus élevé).

Il existe une autre entrée de calcul des coûts, Smart Seek Costing , sur laquelle peu de détails sont connus. Je suppose (et c'est tout ce qu'il en est à ce stade) que SSC tente d'évaluer le coût des E / S côté interne plus en détail, peut-être en tenant compte de l'ordre local et / ou de la plage de valeurs à récupérer. Qui sait.

Par exemple, la première opération de recherche apporte non seulement la ligne demandée, mais toutes les lignes de cette page (dans l'ordre d'index). Compte tenu du modèle d'accès global, la récupération des 1000 lignes dans 1000 recherches ne nécessite que 2 lectures physiques, même avec la lecture anticipée et la prélecture désactivées. De ce point de vue, le coût par défaut des E / S représente une surestimation significative et le coût ajusté par SSC est plus proche de la réalité.

Il semble raisonnable de s'attendre à ce que SSC soit plus efficace lorsque la boucle entraîne une recherche d'index plus ou moins directement et que la référence externe de jointure est la base de l'opération de recherche. D'après ce que je peux dire, SSC est toujours tenté pour des opérations physiques appropriées, mais le plus souvent ne produit aucun ajustement à la baisse lorsque la recherche est séparée de la jointure par d'autres opérations. Les filtres simples sont une exception à cela, peut-être parce que SQL Server peut souvent les pousser dans l'opérateur d'accès aux données. Dans tous les cas, l'optimiseur a un support assez profond pour les sélections.

Il est regrettable que le calcul scalaire pour les projections externes de la sous-requête semble interférer avec SSC ici. Les scalaires de calcul sont généralement déplacés au-dessus de la jointure, mais ceux-ci doivent rester où ils sont. Même ainsi, la plupart des scalaires de calcul normaux sont assez transparents pour l'optimisation, c'est donc un peu surprenant.

Quoi qu'il en soit, lorsque l'opération physique PhyOp_Rangeest produite à partir d'une simple sélection sur un indice SelIdxToRng, SSC est efficace. Lorsque le plus complexe SelToIdxStrategy(sélection sur une table d'une stratégie d'index) est utilisé, le résultat PhyOp_Ranges'exécute SSC mais n'entraîne aucune réduction. Encore une fois, il semble que des opérations plus simples et plus directes fonctionnent mieux avec SSC.

J'aimerais pouvoir vous dire exactement ce que fait SSC et montrer les calculs exacts, mais je ne connais pas ces détails. Si vous souhaitez explorer la sortie de trace limitée disponible pour vous-même, vous pouvez utiliser l'indicateur de trace non documenté 2398. Un exemple de sortie est:

Coût de recherche intelligent (7,1) :: 1,34078e + 154, 0,001

Cet exemple concerne le groupe de mémos 7, variante 1, montrant une limite supérieure de coût et un facteur de 0,001. Pour voir des facteurs plus propres, assurez-vous de reconstruire les tableaux sans parallélisme afin que les pages soient aussi denses que possible. Sans cela, le facteur ressemble plus à 0,000821 pour votre exemple de table cible. Il y a bien sûr des relations assez évidentes.

SSC peut également être désactivé avec l'indicateur de trace non documenté 2399. Avec cet indicateur actif, les deux coûts ont la valeur la plus élevée.

Paul White dit GoFundMonica
la source
8

Pas sûr que ce soit une réponse mais c'est un peu long pour un commentaire. La cause de la différence est de la pure spéculation de ma part et peut peut-être faire réfléchir les autres.

Requêtes simplifiées avec plans d'exécution.

SELECT S.KeyCol, 
       S.OtherCol,
       T.*
FROM staging AS S 
  LEFT OUTER JOIN Target AS T 
    ON T.KeyCol = S.KeyCol;

SELECT S.KeyCol, 
       S.OtherCol,
       T.*
FROM staging AS S 
  LEFT OUTER JOIN (
                  SELECT *
                  FROM Target
                  ) AS T 
    ON T.KeyCol = S.KeyCol;

entrez la description de l'image ici

La principale différence entre ces requêtes équivalentes qui pourraient réellement aboutir à des plans d'exécution identiques est l'opérateur scalaire de calcul. Je ne sais pas pourquoi il doit être là, mais je suppose que c'est aussi loin que l'optimiseur peut aller pour optimiser la table dérivée.

Je suppose que la présence du scalaire de calcul est ce qui amortit le coût d'E / S pour la deuxième requête.

Depuis l' intérieur de l'optimiseur: planifier les coûts

Le coût du processeur est calculé comme 0,0001581 pour la première ligne et 0,000011 pour les lignes suivantes.
...
Le coût d'E / S de 0,003125 est exactement 1/320 - reflétant l'hypothèse du modèle selon laquelle le sous-système de disque peut effectuer 320 opérations d'E / S aléatoires par seconde
...
le composant d'établissement des coûts est suffisamment intelligent pour reconnaître que le nombre total de les pages qui doivent être importées du disque ne peuvent jamais dépasser le nombre de pages requises pour stocker la table entière.

Dans mon cas, le tableau prend 5618 pages et pour obtenir 1000 lignes à partir de 1000000 lignes, le nombre estimé de pages nécessaires est de 5,618, ce qui donne un coût d'E / S de 0,015625.

Le coût du CPU pour les deux requêtes doit être le même 0.0001581 * 1000 executions = 0.1581,.

Ainsi, selon l'article lié ci-dessus, nous pouvons calculer le coût de la première requête à 0,173725.

Et en supposant que j'ai raison sur la façon dont le calcul scalaire fait un gâchis du coût d'E / S, il peut être calculé à 3,2831.

Pas exactement ce qui est montré dans les plans mais c'est juste là dans le quartier.

Mikael Eriksson
la source
6

(Ce serait mieux de commenter la réponse de Paul, mais je n'ai pas encore assez de représentants.)

Je voulais fournir la liste des indicateurs de trace (et quelques DBCCdéclarations) dont j'avais l'habitude de tirer une conclusion proche, au cas où il serait utile d'enquêter sur des divergences similaires à l'avenir. Tous ces éléments ne doivent pas être utilisés en production .

Tout d'abord, j'ai jeté un œil à la note de service finale pour voir quels opérateurs physiques étaient utilisés. Ils se ressemblent certainement selon les plans d'exécution graphiques. J'ai donc utilisé des indicateurs de trace 3604et 8615le premier dirige la sortie vers le client et le second révèle le mémo final:

SELECT S.*, T.KeyCol
FROM Staging AS S
      LEFT OUTER JOIN Target AS T
       ON T.KeyCol = S.KeyCol
OPTION(QUERYTRACEON 3604, -- Output client info
       QUERYTRACEON 8615, -- Shows Final Memo structure
       RECOMPILE);

En remontant depuis le Root Group, j'ai trouvé ces PhyOp_Rangeopérateurs presque identiques :

  1. PhyOp_Range 1 ASC 2.0 Cost(RowGoal 0,ReW 0,ReB 999,Dist 1000,Total 1000)= 0.175559(Distance = 2)
  2. PhyOp_Range 1 ASC 3.0 Cost(RowGoal 0,ReW 0,ReB 999,Dist 1000,Total 1000)= 3.01702(Distance = 2)

La seule différence évidente pour moi était le 2.0et 3.0, qui fait référence à leurs "groupe de mémo 2, original" et "groupe de mémo 3, original" respectifs. En vérifiant le mémo, ceux-ci se réfèrent à la même chose - donc aucune différence n'a encore été révélée.

Deuxièmement, j'ai examiné tout un tas de drapeaux de trace qui se sont avérés vains pour moi - mais qui ont un contenu intéressant. J'ai soulevé le plus de Benjamin Nevarez . Je cherchais des indices sur les règles d'optimisation appliquées dans un cas et non dans l'autre.

 SELECT S.*, T.KeyCol
 FROM Staging AS S
      LEFT OUTER JOIN Target AS T
        ON T.KeyCol = S.KeyCol
 OPTION (QUERYTRACEON 3604, -- Output info to client
         QUERYTRACEON 2363, -- Show stats and cardinality info
         QUERYTRACEON 8675, -- Show optimization process info
         QUERYTRACEON 8606, -- Show logical query trees
         QUERYTRACEON 8607, -- Show physical query tree
         QUERYTRACEON 2372, -- Show memory utilization info for optimization stages 
         QUERYTRACEON 2373, -- Show memory utilization info for applying rules
         RECOMPILE );

Troisièmement, j'ai examiné les règles qui ont été appliquées pour nos PhyOp_Ranges qui semblent si similaires. J'ai utilisé quelques indicateurs de trace mentionnés par Paul dans un article de blog .

SELECT S.*, T.KeyCol
FROM Staging AS S
      LEFT OUTER JOIN (SELECT KeyCol
                      FROM Target) AS T
       ON T.KeyCol = S.KeyCol
OPTION (QUERYTRACEON 3604, -- Output info to client
        QUERYTRACEON 8619, -- Show applied optimization rules
        QUERYTRACEON 8620, -- Show rule-to-memo info
        QUERYTRACEON 8621, -- Show resulting tree
        QUERYTRACEON 2398, -- Show "smart seek costing"
        RECOMPILE );

De la sortie, on voit que le DIRECT - JOINappliqué cette règle pour obtenir notre PhyOp_Rangeopérateur: Rule Result: group=7 2 <SelIdxToRng>PhyOp_Range 1 ASC 2 (Distance = 2). Le subselect a appliqué cette règle à la place: Rule Result: group=9 2 <SelToIdxStrategy>PhyOp_Range 1 ASC 3 (Distance = 2). C'est également là que vous voyez les informations de «recherche intelligente des coûts» associées à chaque règle. Pour le DIRECT - JOINce est la sortie (pour moi): Smart seek costing (7.2) :: 1.34078e+154 , 0.001. Pour le sous - sélection, c'est la sortie: Smart seek costing (9.2) :: 1.34078e+154 , 1.

En fin de compte, je n'ai pas pu conclure grand chose - mais la réponse de Paul comble la majeure partie de l'écart. J'aimerais voir plus d'informations sur les coûts de recherche intelligente.

Steven Hibble
la source
4

Ce n'est pas vraiment une réponse non plus - comme Mikael l'a noté, il est difficile de discuter de ce problème dans les commentaires ...

Fait intéressant, si vous convertissez la sous-requête (select KeyCol FROM Target)en un TVF en ligne, vous voyez que le plan et ses coûts sont les mêmes que la requête d'origine simple:

CREATE FUNCTION dbo.cs_test()
RETURNS TABLE
WITH SCHEMABINDING
AS 
RETURN (
    SELECT KeyCol FROM dbo.Target
    );

/* "normal" variant */
SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN Target AS T ON T.KeyCol = S.KeyCol;

/* "subquery" variant */
SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN (SELECT KeyCol FROM Target) AS T ON T.KeyCol = S.KeyCol;

/* "inline-TVF" variant */
SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN dbo.cs_test() t ON s.KeyCol = t.Keycol

Les plans de requête ( lien pastetheplan ):

entrez la description de l'image ici

La déduction m'amène à croire que le moteur de calcul des coûts est confus quant à l' impact potentiel que ce type de sous-requête peut avoir .

Prenez par exemple ce qui suit:

SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN (
        SELECT KeyCol = CHECKSUM(NEWID()) 
        FROM Target
        ) AS T ON T.KeyCol = S.KeyCol;

Combien coûteriez- vous cela? L'optimiseur de requêtes choisit un plan très similaire à la variante "sous-requête" ci-dessus, contenant un scalaire de calcul ( lien pastetheplan.com ):

entrez la description de l'image ici

Le scalaire de calcul a un coût tout à fait différent de la variante de "sous-requête" présentée ci-dessus, mais il s'agit tout simplement d'une supposition puisque l'optimiseur de requête n'a aucun moyen de savoir, a priori, quel pourrait être le nombre de lignes renvoyées. Le plan utilise une correspondance de hachage pour la jointure externe gauche, car les estimations de ligne sont inconnues et sont donc définies sur le nombre de lignes de la table cible.

entrez la description de l'image ici

Je n'ai pas de grande conclusion à cela, sauf que je suis d'accord avec le travail que Mikael a fait dans sa réponse, et j'espère que quelqu'un d'autre pourra trouver une meilleure réponse.

Max Vernon
la source