Considérez le plan de requête suivant dans SQL Server 2014:
Dans le plan de requête, une jointure auto-rejoindre ar.fId = ar.fId
donne une estimation de 1 rangée. Cependant, il s'agit d'une estimation logiquement incompatible: ar
a 20,608
lignes et juste une valeur distincte de fId
(réfléchi avec précision dans les statistiques). Par conséquent, cette jointure produit le produit croisé complet des lignes (~424MM
lignes), provoquant la course de la requête pendant plusieurs heures.
Je suis difficile de comprendre pourquoi SQL Server proposerait une estimation qui peut être aussi facilement prouvée pour être incompatible avec les statistiques. Des idées?
Enquête initiale et détail supplémentaire
Basé sur Paul's Réponse ici , il semble que les heuristiques SQL 2012 et SQL 2014 pour estimer la jointure Cardinalité devraient facilement gérer une situation où deux histogrammes identiques doivent être comparés.
J'ai commencé avec la sortie du drapeau de trace 2363, mais n'a pas été capable de comprendre cela facilement. Le snippet suivant signifie-t-il que SQL Server comparait des histogrammes pour fId
et bId
afin d'estimer la sélectivité d'une jointure qui utilise uniquement fId
? Si oui, cela ne serait évidemment pas correct. Ou suis-je mal interprété la sortie du drapeau 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 proposé plusieurs solutions de contournement, qui sont incluses dans le script de reproduction complète et apportez cette requête en millisecondes. Cette question est axée 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 devrait être déposé avec Microsoft.
Voici un Script de repro complet , voici la sortie complète du drapeau de trace 236 , et voici la requête et les définitions de table au cas où vous voudrez les regarder 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)
)
Je suis difficile de comprendre pourquoi SQL Server proposerait une estimation qui peut être aussi facilement prouvée pour être incompatible avec les statistiques.
Il n'y a pas de 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'ya rien de mal à la logique qui dit de rejoindre ces deux sous-arbres identiques aurait pour produire un produit croisé, mais il n'ya également rien de dire que le choix du raisonnement est plus sonore que tout autre.
Dans votre cas spécifique, le Initial Estimation de cardinalité pour la jointure est non effectuée sur deux sous-arbres identiques . La forme de l'arbre à cette époque est:
Logop_join [.____] logop_gbaggg Logop_leftouterjoin Logop_get tbl: AR Logop_get Logop_get TBL: TCR SCAUOP_COMP X_CMPEQ [ .____] Scaop_Identifier [TCR] [TCR] Scaop_Const Valeur = 508 [.____] SCAUOP_LOGIQUE X_LOPAND [.____] SCAUOP_COMP X_CMPEQ Scaop_Identifier [AR]. [TCR] .FID SCAUOP_COMP X_CMPEQ [.____] SCAOP_Identififier [AR] .bid SCAUOP_Identififier [TCR] .bid [.____] [.____] ANCOP_PRJEL EXPR1003 Scaop_aggfuncc stopmax [.____] scaop_convert int [.____] Scaop_identifier [TCR] .iss Logop_sélectionnez Logop_gbaggg Logop_get tbl: AR Logop_select Logop_get tbl: tcr [.____] scaop_comp x_cmpeq Scaop_Identififier [TCR] [TCR] [____.] SCAOP_CONST VALUE = 508 [.____] SCAUOP_COMP X_CMPEQ Scaop_Identifier [AR] .FID Scaop_Identififier [TCR] .Fid SCAOP_COMP X_CMPEQ SCAUOP_Identififier [ar] .bid [. ____] .bid Ancop_prjlist [.____] Ancop_prjel EXPR1006 [.____] Scaop_Aggfunc STOPMIN [. ____] SCAOP_CONVERT INT Scaop_aggfunc stopmax [.____] SCAOP_CONVERT INT [.____] SCAUOP_Identififier [TCR] .iss [.____] SCAOP_COMP X_CMPEQ [ .____] Scaop_Identifier EXPR1006 [.____] SCAUOP_CONST VALEUR = 1 [.____] SCAUOP_COMP X_CMPEQ [. ____] SCAUOP_Identifier QCOL: [AR] .FID [. ____ ____]]
La première entrée de jointure a eu un agrégat sans protection simplifié et la seconde entrée de jointure a le prédicat t.isT = 1
Poussé ci-dessous, où t.isT
Est MIN(CONVERT(INT, ar.isT))
. Malgré cela, le calcul de sélectivité pour le prédicat isT
est capable d'utiliser CSelCalcColumnInInterval
sur un histogramme:
CselcalcolumninInterval Colonne: Col: EXPR1006 [.____] Histogramme chargé pour la colonne QCol: [ar] .ist des statistiques avec ID 3 Sélectivité: 4.85248E-005 [.____] Collection Stats générés: Cscollfilter (ID = 11, carte = 1) [. ____] 20608) Cscollouterjoin (ID = 9, carte = 20608 x_JTLEFTOUTER) CscollBastable (ID = 3, carte = 20608 TBL: AR) [.____] Cscollfilter (ID = 8, CARTE = 1 ) CscollBastable (ID = 4, carte = 28 TBL: TCR) [.____]
L'attente (correcte) est de 20 608 lignes à réduire à 1 rangée par ce prédicat.
La question devient maintenant la façon dont les 20 608 lignes de l'autre entrée de la participation correspondront à cette ligne:
Logop_join [.____] cscollgroupby (id = 7, carte = 20608) [.____] cscollouterjoin (id = 6, carte = 20608 x_jtleftouter) [.____] Cscollfilter (ID = 11, carte = 1) Cscollgroupby (ID = 10, carte = 20608) SCAOP_COMP X_CMPEQ [.____] Scaop_Identifier QCol: [ar] .FID Scaop_Identifier QCOL: [ar] .FID [.____]
Il existe différentes manières d'estimer la participation en général. Nous pourrions, par exemple:
En fonction de l'estimateur de cardinalité en cours d'utilisation, et certaines heuristiques, aucune de celles (ou une variation) pourraient être utilisées. Voir le papier blanc Microsoft optimiser vos plans de requête avec l'estimateur de cardinalité SQL Server 2014 Pour plus.
Maintenant, comme indiqué dans la question, dans ce cas, la jointure de la colonne "simple" (sur fId
) utilise la calculatrice CSelCalcExpressionComparedToExpression
:
[. .Bid de statistiques avec ID 2 [.____] Histogramme chargé pour la colonne QCol: [AR] .FId de statistiques avec ID 1 Sélectivité: 0 [.____]
Ce calcul évalue que la jointure des 20 608 lignes avec la rangée filtrée 1 aura une sélectivité zéro: aucune ligne ne correspondra (signalée comme une ligne dans les plans finaux). Est-ce faux? Oui, il y a probablement un bug dans le nouveau CE ici. On pourrait affirmer que 1 ligne correspondra à toutes les lignes ou aucune des lignes, de sorte que le résultat pourrait être raisonnable, mais il y a des raisons de croire autrement.
Les détails sont réellement assez difficiles, mais l'attente de l'estimation est basée sur des histogrammes non filtrés fId
, modifié par la sélectivité du filtre, donnant 20608 * 20608 * 4.85248e-005 = 20608
Les rangées sont très raisonnables.
Suite à ce calcul signifierait à l'aide de la calculatrice CSelCalcSimpleJoinWithDistinctCounts
au lieu de CSelCalcExpressionComparedToExpression
. Il n'y a pas de moyen documenté de le faire, mais si vous êtes curieux, vous pouvez activer le drapeau de trace non documenté 9479:
Remarque La jointure finale produit 20 608 rangées de deux entrées à une seule rangée, mais cela ne devrait pas être une surprise. C'est le même plan produit par le CE d'origine sous TF 9481.
J'ai mentionné que les détails sont délicats (et consommer du temps à enquêter), mais aussi loin que je peux le dire, la cause première du problème est liée au prédicat rId = 508
, Avec une sélectivité zéro. Cette estimation zéro est élevée à une rangée de manière normale, ce qui semble contribuer à l'estimation de la sélectivité zéro lors de la jointure en question lorsqu'elle représente des prédicats plus basses dans l'arborescence d'entrée (par conséquent, des statistiques de chargement pour bId
).
Autoriser la jointure extérieure à conserver une estimation intérieure interne à zéro (au lieu d'augmenter à une rangée) (toutes les lignes extérieures sont-elles qualifiées) donne une estimation de jointure "sans bogue" avec une calculatrice. Si vous êtes intéressé à l'explorer, le drapeau de trace non documenté est 9473 (seul):
Le comportement de l'estimation de la cardinalité de jointure avec CSelCalcExpressionComparedToExpression
peut également être modifié pour ne pas prendre en compte `` BID` avec un autre drapeau de variation non documenté (9494). Je mentionne tout cela parce que je sais que vous avez un intérêt pour de telles choses; pas parce qu'ils offrent une solution. Jusqu'à ce que vous signaliez le problème à Microsoft et qu'ils l'aborde (ou non), exprimant la requête différemment est probablement la meilleure voie à suivre. Indépendamment du fait 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 question est le résultat d'une exploration basée sur des coûts GbAggAfterJoinSel
déplaçant l'agrégat et le filtre au-dessus de la jointure, puisque le Rejoindre la sortie a un tel nombre de lignes. Le filtre était initialement inférieur à la jointure, comme vous êtes attendu.