Je sais que faire COALESCE
sur quelques colonnes et les rejoindre n'est pas une bonne pratique.
La génération de bonnes estimations de cardinalité et de distribution est déjà assez difficile lorsque le schéma est 3NF + (avec clés et contraintes) et que la requête est relationnelle et principalement SPJG (selection-projection-join-group by). Le modèle CE est construit sur ces principes. Plus une fonction est inhabituelle ou non relationnelle dans une requête, plus on se rapproche des limites de ce que le cadre de cardinalité et de sélectivité peut gérer. Allez trop loin et CE abandonnera et devinera .
La plupart de l'exemple MCVE est un SPJ simple (pas de G), bien que les équijoines soient principalement externes (modélisées comme jointure interne plus anti-semi-jointure) plutôt que l'équijoin interne plus simple (ou semi-jointure). Toutes les relations ont des clés, mais pas de clés étrangères ou d'autres contraintes. Toutes les jointures sauf une sont un à plusieurs, ce qui est bien.
L'exception est la jointure externe plusieurs-à-plusieurs entre X_DETAIL_1
et X_DETAIL_LINK
. La seule fonction de cette jointure dans le MCVE est de potentiellement dupliquer des lignes X_DETAIL_1
. C'est une sorte de chose inhabituelle .
Les prédicats d'égalité simples (sélections) et les opérateurs scalaires sont également meilleurs. Par exemple, l' attribut comparaison-égal attribut / constante fonctionne normalement bien dans le modèle. Il est relativement "facile" de modifier les histogrammes et les statistiques de fréquence pour refléter l'application de ces prédicats.
COALESCE
est construit sur CASE
, qui est à son tour implémenté en interne comme IIF
(et cela était vrai bien avant d' IIF
apparaître dans le langage Transact-SQL). Les modèles CE IIF
commeUNION
avec deux enfants mutuellement exclusifs, chacun consistant en un projet sur une sélection sur la relation d'entrée. Chacun des composants répertoriés prend en charge les modèles, il est donc relativement simple de les combiner. Même ainsi, plus il y a d'abstractions d'une couche, moins le résultat final a tendance à être précis - une raison pour laquelle les plans d'exécution plus grands ont tendance à être moins stables et fiables.
ISNULL
, d'autre part, est intrinsèque au moteur. Il n'est pas construit en utilisant des composants plus basiques. L'application de l'effet de ISNULL
à un histogramme, par exemple, est aussi simple que de remplacer l'étape des NULL
valeurs (et de la compacter si nécessaire). Il est encore relativement opaque, comme le font les opérateurs scalaires, et il vaut donc mieux éviter autant que possible. Néanmoins, il est généralement plus convivial pour l'optimiseur (moins hostile à l'optimiseur) qu'un système CASE
alternatif.
Le CE (70 et 120+) est très complexe, même selon les normes SQL Server. Il ne s'agit pas d'appliquer une logique simple (avec une formule secrète) à chaque opérateur. Le CE connaît les clés et les dépendances fonctionnelles; il sait estimer à l'aide de fréquences, de statistiques multivariées et d'histogrammes; et il y a une tonne absolue de cas spéciaux, de raffinements, de freins et contrepoids et de structures de support. Il estime souvent par exemple les jointures de plusieurs manières (fréquence, histogramme) et décide d'un résultat ou d'un ajustement en fonction des différences entre les deux.
Une dernière chose de base à couvrir: l'estimation de cardinalité initiale s'exécute pour chaque opération dans l'arbre de requête, de bas en haut. La sélectivité et la cardinalité sont d'abord dérivées pour les opérateurs feuille (relations de base). Des histogrammes modifiés et des informations de densité / fréquence sont dérivés pour les opérateurs parents. Plus nous montons dans l'arbre, plus la qualité des estimations tend à être faible, car les erreurs ont tendance à s'accumuler.
Cette estimation globale initiale unique fournit un point de départ et se produit bien avant que toute considération ne soit donnée à un plan d'exécution final (elle se produit bien avant même l'étape de compilation du plan trivial). L'arbre de requête à ce stade a tendance à refléter assez étroitement la forme écrite de la requête (bien que les sous-requêtes soient supprimées et les simplifications appliquées, etc.)
Immédiatement après l'estimation initiale, SQL Server effectue un réordonnancement des jointures heuristiques, qui essaie de réorganiser l'arborescence pour placer les tables plus petites et les jointures à haute sélectivité en premier. Il essaie également de positionner les jointures internes avant les jointures externes et les produits croisés. Ses capacités ne sont pas étendues; ses efforts ne sont pas exhaustifs; et il ne prend pas en compte les coûts physiques (car ils n'existent pas encore - seules des informations statistiques et des métadonnées sont présentes). Le réordonnancement heuristique est plus efficace sur les équijoins internes simples. Il existe pour fournir un «meilleur» point de départ pour une optimisation basée sur les coûts.
Pourquoi cette estimation de cardinalité jointe est-elle si importante?
Le MCVE a une jointure plusieurs-à-plusieurs "inhabituelle" pour la plupart redondante , et une jointure équi avec COALESCE
dans le prédicat. L'arborescence des opérateurs a également une jointure interne en dernier , ce qui réordonne la jointure heuristique n'a pas pu déplacer l'arborescence vers une position plus préférée. Laissant de côté tous les scalaires et les projections, l'arbre de jointure est:
LogOp_Join [ Card=4.52803e+009 ]
LogOp_LeftOuterJoin [ Card=481577 ]
LogOp_LeftOuterJoin [ Card=481577 ]
LogOp_LeftOuterJoin [ Card=481577 ]
LogOp_LeftOuterJoin [ Card=481577 ]
LogOp_Get TBL: X_DRIVING_TABLE(alias TBL: dt) [ Card=481577 ]
LogOp_Get TBL: X_DETAIL_1(alias TBL: d1) [ Card=70 ]
LogOp_Get TBL: X_DETAIL_LINK(alias TBL: lnk) [ Card=47 ]
LogOp_Get TBL: X_DETAIL_2(alias TBL: d2) X_DETAIL_2 [ Card=119 ]
LogOp_Get TBL: X_DETAIL_3(alias TBL: d3) X_DETAIL_3 [ Card=281 ]
LogOp_Get TBL: X_LAST_TABLE(alias TBL: lst) X_LAST_TABLE [ Card=94025 ]
Notez que l'estimation finale erronée est déjà en place. Il est imprimé Card=4.52803e+009
et stocké en interne en tant que valeur à virgule flottante double précision 4,5280277425e + 9 (4528027742,5 en décimal).
La table dérivée de la requête d'origine a été supprimée et les projections normalisées. Une représentation SQL de l'arbre sur lequel la cardinalité et l'estimation de sélectivité initiales ont été effectuées est:
SELECT
PRIMARY_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)
FROM X_DRIVING_TABLE dt
LEFT OUTER JOIN X_DETAIL_1 d1
ON dt.ID = d1.ID
LEFT OUTER JOIN X_DETAIL_LINK lnk
ON d1.LINK_ID = lnk.LINK_ID
LEFT OUTER JOIN X_DETAIL_2 d2
ON dt.ID = d2.ID
LEFT OUTER JOIN X_DETAIL_3 d3
ON dt.ID = d3.ID
INNER JOIN X_LAST_TABLE lst
ON lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)
(En passant, la répétition COALESCE
est également présente dans le plan final - une fois dans le calcul final scalaire, et une fois sur le côté intérieur de la jointure intérieure).
Remarquez la jointure finale. Cette jointure interne est (par définition) le produit cartésien de X_LAST_TABLE
et la sortie de jointure précédente, avec une sélection (prédicat de jointure) lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)
appliquée. La cardinalité du produit cartésien est simplement 481577 * 94025 = 45280277425.
Pour cela, nous devons déterminer et appliquer la sélectivité du prédicat. La combinaison de l' COALESCE
arbre étendu opaque (en termes de, UNION
et IIF
rappelez-vous) ainsi que l'impact sur les informations clés, les histogrammes dérivés et les fréquences de la jointure externe plusieurs-à-plusieurs "inhabituelle" principalement-redondante combinée signifie que le CE est incapable de dériver une estimation acceptable de l'une des manières normales.
En conséquence, il entre dans la logique Guess. La logique de la supposition est modérément complexe, avec des couches d'algorithmes de supposition «éduqués» et «moins instruits» essayés. Si aucune meilleure base pour une estimation n'est trouvée, le modèle utilise une estimation de dernier recours, qui pour une comparaison d'égalité est: sqllang!x_Selectivity_Equal
= sélectivité fixe de 0,1 (estimation de 10%):
-- the moment of doom
movsd xmm0,mmword ptr [sqllang!x_Selectivity_Equal
Le résultat est une sélectivité de 0,1 sur le produit cartésien: 481577 * 94025 * 0,1 = 4528027742,5 (~ 4,52803e + 009) comme mentionné précédemment.
Réécrit
Lorsque la jointure problématique est commentée , une meilleure estimation est produite car la sélectivité fixe "deviner le dernier recours" est évitée (les informations clés sont conservées par les jointures 1-M). La qualité de l'estimation est encore peu fiable, car un COALESCE
prédicat de jointure n'est pas du tout compatible CE. L'estimation révisée fait au moins apparence plus raisonnable pour les humains, je suppose.
Lorsque la requête est écrite avec la jointure externe X_DETAIL_LINK
placée en dernier , le réordonnancement heuristique peut l'échanger avec la jointure interne finale vers X_LAST_TABLE
. Placer la jointure interne juste à côté du joint externe problématique donne aux capacités limitées de réorganisation précoce la possibilité d'améliorer l'estimation finale, car les effets de la jointure externe plusieurs-à-plusieurs "inhabituelle" pour la plupart redondante surviennent après l'estimation délicate de la sélectivité. pour COALESCE
. Encore une fois, les estimations ne valent guère mieux que les suppositions fixes et ne résisteraient probablement pas à un contre-interrogatoire déterminé devant un tribunal.
La réorganisation d'un mélange de jointures internes et externes est difficile et prend du temps (même l'optimisation complète de l'étape 2 ne tente qu'un sous-ensemble limité de mouvements théoriques).
La ISNULL
suggestion imbriquée dans la réponse de Max Vernon parvient à éviter la supposition fixe du renflouement, mais l'estimation finale est une improbable ligne zéro (élevée à une ligne pour la décence). Cela pourrait aussi bien être une estimation fixe de 1 ligne, pour toute la base statistique du calcul.
Je m'attendrais à une estimation de cardinalité de jointure entre 0 et 481577 lignes.
C'est une attente raisonnable, même si l'on admet que l'estimation de la cardinalité peut se produire à différents moments (lors de l'optimisation basée sur les coûts) sur des sous-arbres physiquement différents, mais logiquement et sémantiquement identiques - le plan final étant une sorte de meilleur assemblé meilleur (par groupe de mémos). L'absence d'une garantie de cohérence à l'échelle du plan ne signifie pas qu'une jointure individuelle devrait pouvoir bafouer la respectabilité, je comprends.
D'un autre côté, si nous finissons par deviner le dernier recours , l'espoir est déjà perdu, alors pourquoi s'embêter. Nous avons essayé toutes les astuces que nous connaissions et avons abandonné. Si rien d'autre, l'estimation finale sauvage est un grand signe d'avertissement que tout ne s'est pas bien passé à l'intérieur du CE pendant la compilation et l'optimisation de cette requête.
Lorsque j'ai essayé le MCVE, le 120+ CE a produit une estimation finale de ligne zéro (= 1) (comme le niché ISNULL
) pour la requête d'origine, ce qui est tout aussi inacceptable pour ma façon de penser.
La vraie solution implique probablement un changement de conception, pour permettre des équi-jointures simples sans COALESCE
ou ISNULL
, et idéalement des clés étrangères et d'autres contraintes utiles pour la compilation de requêtes.
bigint
au lieu dedecimal(18, 0)
vous obtiendrez des avantages: 1) utilisez 8 octets au lieu de 9 pour chaque valeur, et 2) utilisez un type de données comparable à l'octet au lieu d'un type de données compressé, ce qui pourrait avoir des implications pour le temps CPU lors de la comparaison des valeurs.