Colonne calculée persistante provoquant l'analyse

9

La conversion d'une colonne régulière en une colonne calculée persistante empêche cette requête de faire des recherches d'index. Pourquoi?

Testé sur plusieurs versions de SQL Server, dont 2016 SP1 CU1.

Repros

Le problème est avec table1, col7.

Les tables et la requête sont une version partielle (et simplifiée) des originaux. Je suis conscient que la requête pourrait être réécrite différemment, et pour une raison quelconque, éviter le problème, mais nous devons éviter de toucher au code, et la question de savoir pourquoi table1ne peut pas être recherchée est toujours d'actualité.

Comme Paul White l'a montré (merci!), La recherche est disponible si elle est forcée, donc la question est: pourquoi la recherche n'est pas choisie par l'optimiseur, et si nous pouvons faire quelque chose différemment pour que la recherche se déroule comme il se doit, sans changer la code?

Pour clarifier la partie problématique, voici l'analyse pertinente du mauvais plan d'exécution:

plan

Alex Friedman
la source

Réponses:

12

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 PLANindice - 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 table1plan 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é.

Paul White 9
la source