SQL Server 2014: une explication pour une estimation de cardinalité auto-jointe incohérente?


27

Considérez le plan de requête suivant dans SQL Server 2014:

entrez la description de l'image ici

Dans le plan de requête, une auto-jointure ar.fId = ar.fIddonne une estimation de 1 ligne. Cependant, il s'agit d'une estimation logiquement incohérente: ara des 20,608lignes et une seule valeur distincte de fId(reflétée avec précision dans les statistiques). Par conséquent, cette jointure produit le produit croisé complet des lignes ( ~424MMlignes), provoquant l'exécution de la requête pendant plusieurs heures.

J'ai du mal à comprendre pourquoi SQL Server proposerait une estimation qui peut être si facilement prouvée comme incompatible avec les statistiques. Des idées?

Enquête initiale et détails supplémentaires

Sur la base de la réponse de Paul ici , il semble que l'heuristique SQL 2012 et SQL 2014 pour estimer la cardinalité de jointure devrait facilement gérer une situation dans laquelle deux histogrammes identiques doivent être comparés.

J'ai commencé avec la sortie de l'indicateur de trace 2363, mais je n'ai pas pu comprendre cela facilement. L'extrait suivant signifie-t-il que SQL Server compare les histogrammes pour fIdet bIdafin d'estimer la sélectivité d'une jointure qui utilise uniquement fId? Si c'est le cas, ce ne serait évidemment pas correct. Ou suis-je en train de mal lire la sortie de l'indicateur de trace?

Plan for computation:
  CSelCalcExpressionComparedToExpression( QCOL: [ar].fId x_cmpEq QCOL: [ar].fId )
Loaded histogram for column QCOL: [ar].bId from stats with id 3
Loaded histogram for column QCOL: [ar].fId from stats with id 1
Selectivity: 0

Notez que j'ai trouvé plusieurs solutions de contournement, qui sont incluses dans le script de repro complet et réduisent cette requête en millisecondes. Cette question se concentre sur la compréhension du comportement, comment l'éviter dans les requêtes futures et déterminer s'il s'agit d'un bogue qui doit être déposé auprès de Microsoft.

Voici un script de repro complet , voici la sortie complète de l'indicateur de trace 2363 , et voici les définitions de requête et de table au cas où vous voudriez les consulter rapidement sans ouvrir le script complet:

WITH cte AS (
    SELECT ar.fId, 
        ar.bId,
        MIN(CONVERT(INT, ar.isT)) AS isT,
        MAX(CONVERT(INT, tcr.isS)) AS isS
    FROM  #SQL2014MinMaxAggregateCardinalityBug_ar ar 
    LEFT OUTER JOIN #SQL2014MinMaxAggregateCardinalityBug_tcr tcr
        ON tcr.rId = 508
        AND tcr.fId = ar.fId
        AND tcr.bId = ar.bId
    GROUP BY ar.fId, ar.bId
)
SELECT s.fId, s.bId, s.isS, t.isS
FROM cte s 
JOIN cte t 
    ON t.fId = s.fId 
    AND t.isT = 1

CREATE TABLE #SQL2014MinMaxAggregateCardinalityBug_ar (
    fId INT NOT NULL,
    bId INT NOT NULL,
    isT BIT NOT NULL
    PRIMARY KEY (fId, bId)
)

CREATE TABLE #SQL2014MinMaxAggregateCardinalityBug_tcr (
    rId INT NOT NULL,
    fId INT NOT NULL,
    bId INT NOT NULL,
    isS BIT NOT NULL
    PRIMARY KEY (rId, fId, bId, isS)
)

Réponses:


23

J'ai du mal à comprendre pourquoi SQL Server proposerait une estimation qui peut être si facilement prouvée comme incompatible avec les statistiques.

Cohérence

Il n'y a aucune garantie générale de cohérence. Les estimations peuvent être calculées sur différents sous-arbres (mais logiquement équivalents) à différents moments, en utilisant différentes méthodes statistiques.

Il n'y a rien de mal à la logique qui dit que la jonction de ces deux sous-arbres identiques devrait produire un produit croisé, mais il n'y a rien non plus à dire que le choix du raisonnement est plus judicieux que tout autre.

Estimation initiale

Dans votre cas spécifique, l' estimation de cardinalité initiale pour la jointure n'est pas effectuée sur deux sous-arbres identiques . La forme de l'arbre à ce moment-là est:

  LogOp_Join
     LogOp_GbAgg
        LogOp_LeftOuterJoin
           LogOp_Get TBL: ar
           LogOp_Select
              LogOp_Get TBL: tcr
              ScaOp_Comp x_cmpEq
                 ScaOp_Identifier [tcr] .rId
                 Valeur ScaOp_Const = 508
           ScaOp_Logical x_lopAnd
              ScaOp_Comp x_cmpEq
                 ScaOp_Identifier [ar] .fId
                 ScaOp_Identifier [tcr] .fId
              ScaOp_Comp x_cmpEq
                 ScaOp_Identifier [ar] .bId
                 ScaOp_Identifier [tcr] .bId
        AncOp_PrjList 
           AncOp_PrjEl Expr1003 
              ScaOp_AggFunc stopMax
                 ScaOp_Convert int
                    ScaOp_Identifier [tcr] .isS
     LogOp_Select
        LogOp_GbAgg
           LogOp_LeftOuterJoin
              LogOp_Get TBL: ar
              LogOp_Select
                 LogOp_Get TBL: tcr
                 ScaOp_Comp x_cmpEq
                    ScaOp_Identifier [tcr] .rId
                    Valeur ScaOp_Const = 508
              ScaOp_Logical x_lopAnd
                 ScaOp_Comp x_cmpEq
                    ScaOp_Identifier [ar] .fId
                    ScaOp_Identifier [tcr] .fId
                 ScaOp_Comp x_cmpEq
                    ScaOp_Identifier [ar] .bId
                    ScaOp_Identifier [tcr] .bId
           AncOp_PrjList 
              AncOp_PrjEl Expr1006 
                 ScaOp_AggFunc stopMin
                    ScaOp_Convert int
                       ScaOp_Identifier [ar] .isT
              AncOp_PrjEl Expr1007 
                 ScaOp_AggFunc stopMax
                    ScaOp_Convert int
                       ScaOp_Identifier [tcr] .isS
        ScaOp_Comp x_cmpEq
           ScaOp_Identifier Expr1006 
           Valeur ScaOp_Const = 1
     ScaOp_Comp x_cmpEq
        ScaOp_Identifier QCOL: [ar] .fId
        ScaOp_Identifier QCOL: [ar] .fId

La première entrée de jointure a eu un agrégat non projeté simplifié, et la deuxième entrée de jointure a le prédicat t.isT = 1poussé en dessous, où t.isTest MIN(CONVERT(INT, ar.isT)). Malgré cela, le calcul de sélectivité pour le isTprédicat peut être utilisé CSelCalcColumnInIntervalsur un histogramme:

  CSelCalcColumnInInterval
      Colonne: COL: Expr1006 

Histogramme chargé pour la colonne QCOL: [ar] .isT à partir des statistiques avec l'ID 3

Sélectivité: 4.85248e-005

Collection de statistiques générée: 
  CStCollFilter (ID = 11, CARD = 1)
      CStCollGroupBy (ID = 10, CARD = 20608)
          CStCollOuterJoin (ID = 9, CARD = 20608 x_jtLeftOuter)
              CStCollBaseTable (ID = 3, CARD = 20608 TBL: ar)
              CStCollFilter (ID = 8, CARD = 1)
                  CStCollBaseTable (ID = 4, CARD = 28 TBL: tcr)

L'attente (correcte) est que 20 608 lignes soient réduites à 1 ligne par ce prédicat.

Rejoignez l'estimation

La question est maintenant de savoir comment les 20 608 lignes de l'autre entrée de jointure correspondront à cette ligne:

  LogOp_Join
      CStCollGroupBy (ID = 7, CARD = 20608)
          CStCollOuterJoin (ID = 6, CARD = 20608 x_jtLeftOuter)
              ...

      CStCollFilter (ID = 11, CARD = 1)
          CStCollGroupBy (ID = 10, CARD = 20608)
              ...

      ScaOp_Comp x_cmpEq
          ScaOp_Identifier QCOL: [ar] .fId
          ScaOp_Identifier QCOL: [ar] .fId

Il existe plusieurs façons d'estimer la jointure en général. On pourrait par exemple:

  • Dérivez de nouveaux histogrammes pour chaque opérateur de plan dans chaque sous-arbre, alignez-les au niveau de la jointure (interpolation des valeurs de pas si nécessaire) et voyez comment ils correspondent; ou
  • Effectuer un alignement «grossier» plus simple des histogrammes (en utilisant des valeurs minimum et maximum, pas pas à pas); ou
  • Calculez des sélectivités distinctes pour les colonnes de jointure seules (à partir de la table de base et sans aucun filtrage), puis ajoutez l'effet de sélectivité du ou des prédicats de non-jointure.
  • ...

Selon l'estimateur de cardinalité utilisé et certaines heuristiques, n'importe lequel (ou une variation) pourrait être utilisé. Consultez le livre blanc de Microsoft sur l' optimisation de vos plans de requête avec l'estimateur de cardinalité SQL Server 2014 pour en savoir plus.

Punaise?

Maintenant, comme indiqué dans la question, dans ce cas, la jointure à colonne simple (on fId) utilise la CSelCalcExpressionComparedToExpressioncalculatrice:

Plan pour le calcul:

  CSelCalcExpressionComparedToExpression [ar] .fId x_cmpEq [ar] .fId

Histogramme chargé pour la colonne QCOL: [ar] .bId à partir des statistiques avec l'ID 2
Histogramme chargé pour la colonne QCOL: [ar] .fId à partir des statistiques avec l'ID 1

Sélectivité: 0

Ce calcul évalue que la jonction des 20 608 lignes avec la 1 ligne filtrée aura une sélectivité nulle: aucune ligne ne correspondra (rapportée comme une ligne dans les plans finaux). Est-ce mal? Oui, il y a probablement un bogue dans le nouveau CE ici. On pourrait faire valoir qu'une ligne correspondra à toutes les lignes ou aucune, donc le résultat peut être raisonnable, mais il y a des raisons de croire le contraire.

Les détails sont en fait assez délicats, mais l'attente pour que l'estimation soit basée sur des fIdhistogrammes non filtrés , modifiés par la sélectivité du filtre, donnant des 20608 * 20608 * 4.85248e-005 = 20608lignes est très raisonnable.

Suivre ce calcul signifierait utiliser la calculatrice CSelCalcSimpleJoinWithDistinctCountsau lieu de CSelCalcExpressionComparedToExpression. Il n'existe aucun moyen documenté de le faire, mais si vous êtes curieux, vous pouvez activer l'indicateur de trace non documenté 9479:

Plan 9479

Notez que la jointure finale produit 20 608 lignes à partir de deux entrées à une seule ligne, mais cela ne devrait pas être une surprise. Il s'agit du même plan produit par le CE d'origine sous TF 9481.

J'ai mentionné que les détails sont difficiles (et longs à enquêter), mais pour autant que je sache, la cause première du problème est liée au prédicat rId = 508, avec une sélectivité nulle. Cette estimation zéro est élevée à une ligne de la manière normale, ce qui semble contribuer à l'estimation de la sélectivité zéro à la jointure en question lorsqu'elle tient compte de prédicats inférieurs dans l'arborescence d'entrée (d'où le chargement des statistiques pour bId).

Permettre à la jointure externe de conserver une estimation côté intérieur de la ligne zéro (au lieu d'augmenter à une ligne) (de sorte que toutes les lignes externes se qualifient) donne une estimation de jointure `` sans bogue '' avec l'une ou l'autre calculatrice. Si vous êtes intéressé à explorer cela, l'indicateur de trace non documenté est 9473 (seul):

Plan 9473

Le comportement de l'estimation de cardinalité de jointure avec CSelCalcExpressionComparedToExpressionpeut également être modifié pour ne pas tenir compte de `` bId '' avec un autre indicateur de variation non documenté (9494). Je mentionne tout cela parce que je sais que vous vous intéressez à de telles choses; pas parce qu'ils offrent une solution. Jusqu'à ce que vous signaliez le problème à Microsoft et qu'ils le résolvent (ou non), exprimer la requête différemment est probablement la meilleure façon de procéder. Peu importe que le comportement soit intentionnel ou non, ils devraient être intéressés à entendre parler de la régression.

Enfin, pour ranger une autre chose mentionnée dans le script de reproduction: la position finale du filtre dans le plan de questions est le résultat d'une exploration basée sur les coûts GbAggAfterJoinSeldéplaçant l'agrégat et le filtre au-dessus de la jointure, car la sortie de la jointure a une si petite Nombre de rangées. Le filtre était initialement sous la jointure, comme vous vous y attendiez.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.