Pourquoi la recherche n'est pas choisie par l'optimiseur
TL: DR La définition de colonne calculée étendue interfère avec la capacité de l'optimiseur à réorganiser les jointures initialement. Avec un point de départ différent, l'optimisation basée sur les coûts emprunte un chemin différent dans l'optimiseur et se termine par un choix de plan final différent.
Détails
Pour toutes les requêtes sauf les plus simples, l'optimiseur n'essaie pas d'explorer quoi que ce soit comme tout l'espace des plans possibles. Au lieu de cela, il choisit un point de départ d' aspect raisonnable , puis dépense une quantité budgétée d'efforts pour explorer les variations logiques et physiques, en une ou plusieurs phases de recherche, jusqu'à ce qu'il trouve un plan raisonnable.
La raison principale pour laquelle vous obtenez des plans différents (avec des estimations de coûts finales différentes) pour les deux cas est qu'il existe des points de départ différents . Partant d'un endroit différent, l'optimisation aboutit à un endroit différent (après son nombre limité d'itérations d'exploration et de mise en œuvre). J'espère que c'est assez intuitif.
Le point de départ que j'ai mentionné est quelque peu basé sur la représentation textuelle de la requête, mais des modifications sont apportées à la représentation de l'arborescence interne lorsqu'elle passe par les étapes d'analyse, de liaison, de normalisation et de simplification de la compilation de requête.
Il est important de noter que le point de départ exact dépend fortement de l' ordre de jointure initial sélectionné par l'optimiseur. Ce choix est effectué avant le chargement des statistiques et avant que des estimations de cardinalité aient été dérivées. La cardinalité totale (nombre de lignes) dans chaque table est cependant connue, ayant été obtenue à partir des métadonnées du système.
L'ordre de jointure initial est donc basé sur l' heuristique . Par exemple, l'optimiseur essaie de réécrire l'arborescence de telle sorte que les petites tables sont jointes avant les plus grandes et les jointures internes avant les jointures externes (et les jointures croisées).
La présence de la colonne calculée interfère avec ce processus, plus précisément avec la capacité de l'optimiseur à pousser les jointures externes vers le bas de l'arbre de requête. Cela est dû au fait que la colonne calculée est développée dans son expression sous-jacente avant que la réorganisation des jointures ne se produise, et déplacer une jointure au-delà d'une expression complexe est beaucoup plus difficile que de la déplacer au-delà d'une simple référence de colonne.
Les arborescences impliquées sont assez grandes, mais pour illustrer, l' arborescence de requête initiale de colonne non calculée commence par: (notez les deux jointures externes en haut)
LogOp_Select
LogOp_Apply (x_jtLeftOuter)
LogOp_LeftOuterJoin
LogOp_NAryJoin
LogOp_LeftAntiSemiJoin
LogOp_NAryJoin
LogOp_Get TBL: dbo.table1 (alias TBL: a4)
LogOp_Select
LogOp_Get TBL: dbo.table6 (alias TBL: a3)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a3] .col18
ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
LogOp_Select
LogOp_Get TBL: dbo.table1 (alias TBL: a1)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a1] .col2
ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
LogOp_Select
LogOp_Get TBL: dbo.table5 (alias TBL: a2)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a2] .col2
ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a4] .col2
ScaOp_Identifier QCOL: [a3] .col19
LogOp_Select
LogOp_Get TBL: dbo.table7 (alias TBL: a7)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a7] .col22
ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a4] .col2
ScaOp_Identifier QCOL: [a7] .col23
LogOp_Select
LogOp_Get TBL: table1 (alias TBL: cdc)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [cdc] .col6
ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, Not Owned, Value = 4)
LogOp_Get TBL: dbo.table5 (alias TBL: a5)
LogOp_Get TBL: table2 (alias TBL: cdt)
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a5] .col2
ScaOp_Identifier QCOL: [cdc] .col2
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a4] .col2
ScaOp_Identifier QCOL: [cdc] .col2
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [cdt] .col1
ScaOp_Identifier QCOL: [cdc] .col1
LogOp_Get TBL: table3 (alias TBL: ahcr)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [ahcr] .col9
ScaOp_Identifier QCOL: [cdt] .col1
Le même fragment de la requête de colonne calculée est: (notez la jointure externe beaucoup plus bas, la définition développée de la colonne calculée et quelques autres différences subtiles dans l'ordre de jointure (interne))
LogOp_Select
LogOp_Apply (x_jtLeftOuter)
LogOp_NAryJoin
LogOp_LeftAntiSemiJoin
LogOp_NAryJoin
LogOp_Get TBL: dbo.table1 (alias TBL: a4)
LogOp_Select
LogOp_Get TBL: dbo.table6 (alias TBL: a3)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a3] .col18
ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
LogOp_Select
LogOp_Get TBL: dbo.table1 (alias TBL: a1
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a1] .col2
ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
LogOp_Select
LogOp_Get TBL: dbo.table5 (alias TBL: a2)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a2] .col2
ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a4] .col2
ScaOp_Identifier QCOL: [a3] .col19
LogOp_Select
LogOp_Get TBL: dbo.table7 (alias TBL: a7)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a7] .col22
ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a4] .col2
ScaOp_Identifier QCOL: [a7] .col23
LogOp_Project
LogOp_LeftOuterJoin
LogOp_Join
LogOp_Select
LogOp_Get TBL: table1 (alias TBL: cdc)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [cdc] .col6
ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, Not Owned, Value = 4)
LogOp_Get TBL: table2 (alias TBL: cdt)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [cdc] .col1
ScaOp_Identifier QCOL: [cdt] .col1
LogOp_Get TBL: table3 (alias TBL: ahcr)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [ahcr] .col9
ScaOp_Identifier QCOL: [cdt] .col1
AncOp_PrjList
AncOp_PrjEl QCOL: [cdc] .col7
ScaOp_Convert char collate 53256, Null, Trim, ML = 6
ScaOp_IIF varchar collate 53256, Null, Var, Trim, ML = 6
ScaOp_Comp x_cmpEq
ScaOp_Intrinsic isnumeric
ScaOp_Intrinsic droite
ScaOp_Identifier QCOL: [cdc] .col4
ScaOp_Const TI (int, ML = 4) XVAR (int, Not Owned, Value = 4)
ScaOp_Const TI (int, ML = 4) XVAR (int, Not Owned, Value = 0)
ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 1) XVAR (varchar, Owned, Value = Len, Data = (0,))
ScaOp_Intrinsic substring
ScaOp_Const TI (int, ML = 4) XVAR (int, Not Owned, Value = 6)
ScaOp_Const TI (int, ML = 4) XVAR (int, Not Owned, Value = 1)
ScaOp_Identifier QCOL: [cdc] .col4
LogOp_Get TBL: dbo.table5 (alias TBL: a5)
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a5] .col2
ScaOp_Identifier QCOL: [cdc] .col2
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [a4] .col2
ScaOp_Identifier QCOL: [cdc] .col2
Les statistiques sont chargées et une estimation de cardinalité initiale est effectuée sur l'arborescence juste après la définition de l'ordre de jointure initial. Le fait d'avoir les jointures dans différents ordres affecte également ces estimations, et a donc un effet d'entraînement lors de l'optimisation ultérieure basée sur les coûts.
Enfin, pour cette section, le fait d'avoir une jointure externe coincée au milieu de l'arborescence peut empêcher certaines règles de réordonnancement des jointures de correspondre pendant l'optimisation basée sur les coûts.
L'utilisation d'un guide de plan (ou, de manière équivalente, un USE PLAN
indice - exemple pour votre requête ) transforme la stratégie de recherche en une approche plus orientée vers les objectifs, guidée par la forme générale et les caractéristiques du modèle fourni. Cela explique pourquoi l'optimiseur peut trouver le même table1
plan de recherche par rapport aux schémas de colonnes calculés et non calculés, lorsqu'un guide de plan ou un conseil est utilisé.
Si nous pouvons faire quelque chose différemment pour que la recherche se réalise
C'est quelque chose dont vous ne devez vous soucier que si l'optimiseur ne trouve pas seul un plan avec des caractéristiques de performances acceptables.
Tous les outils de réglage normaux sont potentiellement applicables. Vous pouvez, par exemple, diviser la requête en parties plus simples, revoir et améliorer l'indexation disponible, mettre à jour ou créer de nouvelles statistiques ... et ainsi de suite.
Toutes ces choses peuvent affecter les estimations de cardinalité, le chemin de code emprunté à l'optimiseur et influencer les décisions basées sur les coûts de manière subtile.
Vous pouvez finalement recourir à des astuces (ou à un guide de plan), mais ce n'est généralement pas la solution idéale.
Questions supplémentaires des commentaires
Je conviens qu'il est préférable de simplifier la requête, etc., mais existe-t-il un moyen (indicateur de trace) de faire en sorte que l'optimiseur poursuive l'optimisation et atteigne le même résultat?
Non, il n'y a pas d'indicateur de trace pour effectuer une recherche exhaustive et vous n'en voulez pas. L'espace de recherche possible est vaste et les temps de compilation qui dépassent l'âge de l'univers ne seraient pas bien reçus. De plus, l'optimiseur ne connaît pas toutes les transformations logiques possibles (personne ne le sait).
Aussi, pourquoi l'expansion complexe est-elle nécessaire, car la colonne est persistante? Pourquoi l'optimiseur ne peut-il pas éviter de le développer, de le traiter comme une colonne régulière et d'atteindre le même point de départ?
Les colonnes calculées sont développées (comme les vues) pour permettre des opportunités d'optimisation supplémentaires. L'expansion peut être mise en correspondance, par exemple, vers une colonne ou un index persistant plus tard dans le processus, mais cela se produit une fois que l' ordre de jointure initial est fixé.