Dies ist eine Entscheidung des kostenbasierten Optimierers.
Die bei dieser Auswahl verwendeten geschätzten Kosten sind falsch, da davon ausgegangen wird, dass die Werte in verschiedenen Spalten statistisch unabhängig sind.
Es ähnelt dem in Row Goals Gone Rogue beschriebenen Problem, bei dem die geraden und ungeraden Zahlen negativ korreliert sind.
Es ist leicht zu reproduzieren.
CREATE TABLE dbo.animal(
id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
colour varchar(50) NOT NULL,
species varchar(50) NOT NULL,
Filler char(10) NULL
);
/*Insert 20 million rows with 1% black and 1% swan but no black swans*/
WITH T
AS (SELECT TOP 20000000 ROW_NUMBER() OVER (ORDER BY @@SPID) AS RN
FROM master..spt_values v1,
master..spt_values v2,
master..spt_values v3)
INSERT INTO dbo.animal
(colour,
species)
SELECT CASE
WHEN RN % 100 = 1 THEN 'black'
ELSE CAST(RN % 100 AS VARCHAR(3))
END,
CASE
WHEN RN % 100 = 2 THEN 'swan'
ELSE CAST(RN % 100 AS VARCHAR(3))
END
FROM T
/*Create some indexes*/
CREATE NONCLUSTERED INDEX ix_species ON dbo.animal(species);
CREATE NONCLUSTERED INDEX ix_colour ON dbo.animal(colour);
Versuchen Sie es jetzt
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black'
AND species LIKE 'swan'
Dies ergibt den Plan, unter dem gerechnet wird 0.0563167
.
Der Plan kann einen Merge-Join zwischen den Ergebnissen der beiden Indizes für die id
Spalte ausführen . ( Weitere Details zum Merge-Join-Algorithmus finden Sie hier ).
Für die Zusammenführung müssen beide Eingaben nach dem Verbindungsschlüssel sortiert werden.
Die nicht gruppierten Indizes sind nach (species, id)
und geordnet (colour, id)
(bei nicht eindeutigen nicht gruppierten Indizes wird der Zeilenlokator immer implizit am Ende des Schlüssels hinzugefügt, wenn er nicht explizit hinzugefügt wird). Die Abfrage ohne Platzhalter führt eine Gleichheitssuche in species = 'swan'
und durch colour ='black'
. Da bei jeder Suche nur ein exakter Wert aus der führenden Spalte abgerufen wird, werden die übereinstimmenden Zeilen nach id
diesem Plan sortiert .
Abfrageplanoperatoren werden von links nach rechts ausgeführt . Der linke Operator fordert Zeilen von seinen untergeordneten Elementen an, die wiederum Zeilen von ihren untergeordneten Elementen anfordern (und so weiter, bis die Blattknoten erreicht sind). Der TOP
Iterator fordert nach dem Empfang von 10 keine weiteren Zeilen mehr von seinem untergeordneten Element an.
SQL Server verfügt über Statistiken zu den Indizes, aus denen hervorgeht, dass 1% der Zeilen mit jedem Prädikat übereinstimmen. Es wird davon ausgegangen, dass diese Statistiken unabhängig sind (dh weder positiv noch negativ korreliert sind), sodass im Durchschnitt nach Verarbeitung von 1.000 Zeilen, die mit dem ersten Prädikat übereinstimmen, 10 Zeilen gefunden werden, die mit dem zweiten übereinstimmen, und beendet werden können. (Der Plan oben zeigt tatsächlich 987 anstatt 1.000, aber nahe genug).
Da die Prädikate negativ korreliert sind, zeigt der tatsächliche Plan, dass alle 200.000 übereinstimmenden Zeilen aus jedem Index verarbeitet werden mussten. Dies wird jedoch bis zu einem gewissen Grad verringert, da die null verbundenen Zeilen auch bedeuten, dass tatsächlich keine Suchvorgänge erforderlich waren.
Vergleichen mit
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
Womit sich der Plan ergibt, unter dem gerechnet wird 0.567943
Das Hinzufügen des nachfolgenden Platzhalters hat jetzt einen Index-Scan verursacht. Die Kosten des Plans sind jedoch für einen Scan einer 20-Millionen-Zeilentabelle immer noch recht niedrig.
Beim Hinzufügen querytraceon 9130
werden weitere Informationen angezeigt
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
OPTION (QUERYTRACEON 9130)
Es ist zu sehen, dass SQL Server nach Schätzungen nur etwa 100.000 Zeilen scannen muss, bevor 10 gefunden werden, die mit dem Prädikat übereinstimmen, und die TOP
Anforderung von Zeilen stoppen kann.
Auch dies ist bei der Unabhängigkeitsannahme als sinnvoll 10 * 100 * 100 = 100,000
Lassen Sie uns zum Schluss versuchen, einen Indexschnittplan zu erzwingen
SELECT TOP 10 *
FROM animal WITH (INDEX(ix_species), INDEX(ix_colour))
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
Dies ergibt für mich einen parallelen Plan mit geschätzten Kosten von 3,4625
Der Hauptunterschied besteht darin, dass das colour like 'black%'
Prädikat nun mehreren verschiedenen Farben entsprechen kann. Dies bedeutet, dass die übereinstimmenden Indexzeilen für dieses Prädikat nicht mehr garantiert in der Reihenfolge sortiert werden id
.
Beispielsweise kann die Indexsuche nach like 'black%'
die folgenden Zeilen zurückgeben
+------------+----+
| Colour | id |
+------------+----+
| black | 12 |
| black | 20 |
| black | 23 |
| black | 25 |
| blackberry | 1 |
| blackberry | 50 |
+------------+----+
Innerhalb jeder Farbe sind die IDs geordnet, aber die IDs in verschiedenen Farben können durchaus nicht sein.
Infolgedessen kann SQL Server keinen Merge-Join-Indexschnitt mehr ausführen (ohne einen blockierenden Sortieroperator hinzuzufügen) und entscheidet sich stattdessen für einen Hash-Join. Hash Join blockiert die Build-Eingabe, sodass die Kosten jetzt die Tatsache widerspiegeln, dass alle übereinstimmenden Zeilen aus der Build-Eingabe verarbeitet werden müssen, anstatt davon auszugehen, dass nur 1.000 wie im ersten Plan gescannt werden müssen.
Der Testeingang ist jedoch nicht blockierend und schätzt immer noch fälschlicherweise, dass er in der Lage sein wird, den Test abzubrechen, nachdem 987 Zeilen davon verarbeitet wurden.
(Weitere Informationen zu nicht blockierenden und blockierenden Iteratoren hier)
Angesichts der gestiegenen Kosten für die zusätzlichen geschätzten Zeilen und den Hash-Join sieht der Teil-Clustered-Index-Scan günstiger aus.
In der Praxis ist der "partielle" Clustered-Index-Scan natürlich überhaupt nicht partiell und muss die gesamten 20 Millionen Zeilen durchlaufen, anstatt die beim Vergleich der Pläne angenommenen 100.000.
Durch Erhöhen des Werts von TOP
(oder durch vollständiges Entfernen) wird schließlich ein Wendepunkt erreicht, an dem die Anzahl der Zeilen, die der CI-Scan voraussichtlich abdecken muss, das Erscheinungsbild des Plans verteuert und der Plan für die Indexkreuzung wiederhergestellt wird. Für mich den Grenzpunkt zwischen den beiden Plänen ist TOP (89)
vs TOP (90)
.
Für Sie kann es durchaus unterschiedlich sein, da es davon abhängt, wie breit der Clustered-Index ist.
TOP
CI-Scan entfernen und erzwingen
SELECT *
FROM animal WITH (INDEX = 1)
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
Wird 88.0586
auf meinem Rechner für meinen Beispieltisch berechnet.
Wenn SQL Server wissen würde, dass der Zoo keine schwarzen Schwäne hat und einen vollständigen Scan durchführen müsste, anstatt nur 100.000 Zeilen zu lesen, würde dieser Plan nicht gewählt.
Ich habe mehrspaltigen Statistik anprobiert animal(species,colour)
und animal(colour,species)
und Statistiken gefiltert auf , animal (colour) where species = 'swan'
aber keiner von ihnen ihm helfen , davon überzeugen , dass schwarze Schwäne nicht existieren und die TOP 10
Scan müssen mehr als 100.000 Zeilen verarbeiten.
Dies ist auf die "Einschlussannahme" zurückzuführen, bei der SQL Server im Wesentlichen davon ausgeht, dass bei der Suche nach etwas möglicherweise etwas vorhanden ist.
Ab 2008 gibt es ein dokumentiertes Ablaufverfolgungsflag 4138 , das Zeilenziele deaktiviert . Dies hat zur Folge, dass der Plan ohne die Annahme kalkuliert TOP
wird, dass die untergeordneten Operatoren vorzeitig kündigen können, ohne alle übereinstimmenden Zeilen zu lesen. Mit dieser Ablaufverfolgungsmarkierung erhalte ich natürlich den optimaleren Indexschnittplan.
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
OPTION (QUERYTRACEON 4138)
Dieser Plan kostet nun korrekterweise für das Lesen der gesamten 200.000 Zeilen in beiden Indexsuchen, überfordert jedoch die Schlüsselsuchvorgänge (geschätzte 2.000 gegenüber der tatsächlichen 0. TOP 10
Dies würde auf ein Maximum von 10 beschränkt, aber das Ablaufverfolgungsflag verhindert, dass dies berücksichtigt wird.) . Trotzdem ist der Plan deutlich günstiger als der vollständige CI-Scan, so dass ausgewählt wird.
Natürlich könnte dieser Plan nicht für Kombinationen optimal sein , die sind weit verbreitet. Wie weiße Schwäne.
Ein zusammengesetzter Index für animal (colour, species)
oder im Idealfall animal (species, colour)
würde es ermöglichen, dass die Abfrage für beide Szenarien wesentlich effizienter ist.
Um den zusammengesetzten Index möglichst effizient zu nutzen, LIKE 'swan'
müsste auch auf geändert werden = 'swan'
.
In der folgenden Tabelle sind die in den Ausführungsplänen für alle vier Permutationen angegebenen Such- und Restprädikate aufgeführt.
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
| WHERE clause | Index | Seek Predicate | Residual Predicate |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
| colour LIKE 'black%' AND species LIKE 'swan' | ix_colour_species | colour >= 'black' AND colour < 'blacL' | colour like 'black%' AND species like 'swan' |
| colour LIKE 'black%' AND species LIKE 'swan' | ix_species_colour | species >= 'swan' AND species <= 'swan' | colour like 'black%' AND species like 'swan' |
| colour LIKE 'black%' AND species = 'swan' | ix_colour_species | (colour,species) >= ('black', 'swan')) AND colour < 'blacL' | colour LIKE 'black%' AND species = 'swan' |
| colour LIKE 'black%' AND species = 'swan' | ix_species_colour | species = 'swan' AND (colour >= 'black' and colour < 'blacL') | colour like 'black%' |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
TOP
Wert in einer Variablen verschleiern, wird diesTOP 100
eher als angenommenTOP 10
. Dies kann hilfreich sein oder auch nicht, je nachdem, wie hoch der Wendepunkt zwischen den beiden Plänen ist.