web-dev-qa-db-fra.com

SQL Server 2014: Toute explication de l'estimation de la cardinalité de soi incohérent?

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

enter image description here

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)
)
27
Geoff Patterson

Je suis difficile de comprendre pourquoi SQL Server proposerait une estimation qui peut être aussi facilement prouvée pour être incompatible avec les statistiques.

Cohérence

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.

Estimation initiale

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.

Estimation

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:

  • Dériver de nouveaux histogrammes à chaque opérateur de plan de chaque sous-arbre, alignez-les à la jointure (valeurs d'étape interpolant si nécessaire) et voyez comment elles correspondent; ou alors
  • Effectuer un alignement plus simple des histogrammes (en utilisant des valeurs minimum et maximum, pas pas à pas); ou alors
  • Calculez des sélectivités séparées pour les colonnes de jointure seules (à partir de la table de base et sans filtrage), puis ajoutez l'effet de sélectivité du ou des prédicats non rejoignés.
  • ...

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.

Punaise?

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:

9479 plan

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):

9473 plan

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.

23
Paul White 9