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 = 1
poussé en dessous, où t.isT
est MIN(CONVERT(INT, ar.isT))
. Malgré cela, le calcul de sélectivité pour le isT
prédicat peut être utilisé CSelCalcColumnInInterval
sur 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 CSelCalcExpressionComparedToExpression
calculatrice:
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 fId
histogrammes non filtrés , modifiés par la sélectivité du filtre, donnant des 20608 * 20608 * 4.85248e-005 = 20608
lignes est très raisonnable.
Suivre ce calcul signifierait utiliser la calculatrice CSelCalcSimpleJoinWithDistinctCounts
au 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:
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):
Le comportement de l'estimation de cardinalité de jointure avec CSelCalcExpressionComparedToExpression
peut é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 GbAggAfterJoinSel
dé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.