Anhaltende berechnete Spalte, die einen Scan verursacht

9

Das Konvertieren einer regulären Spalte in eine persistierte berechnete Spalte führt dazu, dass diese Abfrage keine Indexsuchen durchführen kann. Warum?

Getestet auf mehreren SQL Server-Versionen, einschließlich 2016 SP1 CU1.

Repros

Das Problem ist mit table1, col7.

Die Tabellen und Abfragen sind eine teilweise (und vereinfachte) Version der Originale. Ich bin mir bewusst, dass die Abfrage anders geschrieben werden könnte und aus irgendeinem Grund das Problem vermeiden könnte, aber wir müssen vermeiden, den Code zu berühren, und die Frage, warum table1nicht gesucht werden kann, bleibt bestehen.

Wie Paul White gezeigt hat (danke!), Ist die Suche verfügbar, wenn sie erzwungen wird. Die Frage lautet also: Warum wird die Suche nicht vom Optimierer ausgewählt und ob wir etwas anderes tun können, um die Suche so zu gestalten, wie sie sollte, ohne die zu ändern Code?

Um den problematischen Teil zu verdeutlichen, ist hier der relevante Scan im Plan für schlechte Ausführung:

planen

Alex Friedman
quelle

Antworten:

12

Warum die Suche nicht vom Optimierer ausgewählt wird


TL: DR Die erweiterte berechnete Spaltendefinition beeinträchtigt die Fähigkeit des Optimierers, Verknüpfungen zunächst neu anzuordnen. Mit einem anderen Ausgangspunkt nimmt die kostenbasierte Optimierung einen anderen Weg durch das Optimierungsprogramm und führt zu einer anderen endgültigen Planauswahl.


Einzelheiten

Bei allen außer den einfachsten Abfragen versucht der Optimierer nicht, den gesamten Bereich möglicher Pläne zu untersuchen. Stattdessen wird ein vernünftig aussehender Ausgangspunkt ausgewählt und anschließend in einer oder mehreren Suchphasen mit einem begrenzten Aufwand logische und physische Variationen untersucht, bis ein vernünftiger Plan gefunden ist.

Der Hauptgrund, warum Sie für beide Fälle unterschiedliche Pläne (mit unterschiedlichen endgültigen Kostenschätzungen) erhalten, ist, dass es unterschiedliche Ausgangspunkte gibt. Ausgehend von einem anderen Ort endet die Optimierung an einem anderen Ort (nach einer begrenzten Anzahl von Explorations- und Implementierungsiterationen). Ich hoffe das ist einigermaßen intuitiv.

Der von mir erwähnte Ausgangspunkt basiert in gewisser Weise auf der Textdarstellung der Abfrage, es werden jedoch Änderungen an der internen Baumdarstellung vorgenommen, während diese die Phasen der Analyse, Bindung, Normalisierung und Vereinfachung der Abfragekompilierung durchläuft.

Wichtig ist, dass der genaue Startpunkt stark von der vom Optimierer ausgewählten anfänglichen Verknüpfungsreihenfolge abhängt . Diese Auswahl wird getroffen, bevor Statistiken geladen werden und bevor Kardinalitätsschätzungen abgeleitet wurden. Die Gesamtkardinalität (Anzahl der Zeilen) in jeder Tabelle ist jedoch bekannt, da sie aus Systemmetadaten erhalten wurde.

Die anfängliche Join-Reihenfolge basiert daher auf Heuristiken . Das Optimierungsprogramm versucht beispielsweise, den Baum so umzuschreiben, dass kleinere Tabellen vor größeren verknüpft werden und innere Verknüpfungen vor äußeren Verknüpfungen (und Kreuzverknüpfungen) erfolgen.

Das Vorhandensein der berechneten Spalte stört diesen Prozess, insbesondere die Fähigkeit des Optimierers, äußere Verknüpfungen in den Abfragebaum zu verschieben. Dies liegt daran, dass die berechnete Spalte vor der Neuordnung der Verknüpfung in ihren zugrunde liegenden Ausdruck erweitert wird und das Verschieben einer Verknüpfung an einem komplexen Ausdruck vorbei viel schwieriger ist als das Verschieben an einer einfachen Spaltenreferenz.

Die beteiligten Bäume sind ziemlich groß, aber zur Veranschaulichung beginnt der nicht berechnete anfängliche Abfragebaum der Spalte mit: (Beachten Sie die beiden äußeren Verknüpfungen oben)

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, nicht im Besitz, Wert = 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

Das gleiche Fragment der berechneten Spaltenabfrage lautet: (Beachten Sie die äußere Verknüpfung viel weiter unten, die erweiterte Definition der berechneten Spalte und einige andere subtile Unterschiede in der (inneren) Verknüpfungsreihenfolge.)

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, nicht im Besitz, Wert = 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 ist numerisch
                                        ScaOp_Intrinsic richtig
                                            ScaOp_Identifier QCOL: [cdc] .col4
                                            ScaOp_Const TI (int, ML = 4) XVAR (int, nicht im Besitz, Wert = 4)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, nicht im Besitz, Wert = 0)
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 1) XVAR (varchar, Owned, Value = Len, Data = (0,))
                                ScaOp_Intrinsic-Teilzeichenfolge
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, nicht im Besitz, Wert = 6)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, nicht im Besitz, Wert = 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

Statistiken werden geladen und eine anfängliche Kardinalitätsschätzung wird für den Baum durchgeführt, unmittelbar nachdem die anfängliche Verknüpfungsreihenfolge festgelegt wurde. Die Verknüpfungen in unterschiedlichen Reihenfolgen wirken sich auch auf diese Schätzungen aus und wirken sich daher auf die spätere kostenbasierte Optimierung aus.

Wenn in diesem Abschnitt ein äußerer Join in der Mitte des Baums steckt, kann dies verhindern, dass während der kostenbasierten Optimierung weitere Regeln für die Neuordnung von Joins neu angeordnet werden.


Durch die Verwendung eines Planleitfadens (oder gleichwertig eines USE PLANHinweises - Beispiel für Ihre Abfrage ) wird die Suchstrategie in einen zielorientierteren Ansatz geändert , der sich an der allgemeinen Form und den Merkmalen der bereitgestellten Vorlage orientiert. Dies erklärt , warum der Optimierer kann das gleiche finden table1suchen Plan gegen beide berechnet und nicht berechnete Spalte Schemata, wenn ein Plan Anleitung oder Hinweis verwendet wird.

Ob wir etwas anders machen können, um die Suche zu ermöglichen

Dies ist etwas, worüber Sie sich nur Sorgen machen müssen, wenn der Optimierer selbst keinen Plan mit akzeptablen Leistungsmerkmalen findet.

Alle normalen Tuning-Tools sind möglicherweise anwendbar. Sie können beispielsweise die Abfrage in einfachere Teile aufteilen, die verfügbare Indizierung überprüfen und verbessern, Statistiken aktualisieren oder neue Statistiken erstellen ... und so weiter.

All diese Dinge können sich auf Kardinalitätsschätzungen und den Codepfad durch den Optimierer auswirken und kostenbasierte Entscheidungen auf subtile Weise beeinflussen.

Sie können letztendlich auf Hinweise (oder einen Planleitfaden) zurückgreifen, aber das ist normalerweise nicht die ideale Lösung.


Zusätzliche Fragen aus Kommentaren

Ich bin damit einverstanden, dass es am besten ist, die Abfrage usw. zu vereinfachen. Gibt es jedoch eine Möglichkeit (Trace-Flag), das Optimierungsprogramm mit der Optimierung fortzusetzen und das gleiche Ergebnis zu erzielen?

Nein, es gibt kein Trace-Flag, um eine umfassende Suche durchzuführen, und Sie möchten keines. Der mögliche Suchraum ist riesig und Kompilierungszeiten, die das Alter des Universums überschreiten, würden nicht gut angenommen. Außerdem kennt der Optimierer nicht jede mögliche logische Transformation (niemand kennt sie).

Warum ist die komplexe Erweiterung erforderlich, da die Spalte bestehen bleibt? Warum kann das Optimierungsprogramm es nicht vermeiden, es zu erweitern, es wie eine normale Spalte zu behandeln und denselben Ausgangspunkt zu erreichen?

Berechnete Spalten werden erweitert (wie Ansichten), um zusätzliche Optimierungsmöglichkeiten zu ermöglichen. Die Erweiterung kann später im Prozess möglicherweise auf eine persistente Spalte oder einen Index zurückgeführt werden. Dies geschieht jedoch, nachdem die anfängliche Verknüpfungsreihenfolge festgelegt wurde.

Paul White 9
quelle