Enorme Verlangsamung der SQL Server-Abfrage beim Hinzufügen von Platzhaltern (oder oben)


52

Ich habe einen Zoo mit 20 Millionen Tieren, die ich in meiner SQL Server 2005-Datenbank verfolge. Ungefähr 1% von ihnen sind schwarz und ungefähr 1% von ihnen sind Schwäne. Ich wollte Einzelheiten über alle schwarzen Schwäne erfahren und wollte die Ergebnisseite, die ich erstellt habe, nicht überfluten:

select top 10 * 
from animal 
where colour like 'black'  
and species like 'swan'

(Ja, es wird davon abgeraten, dass diese Felder Freitext sind, aber beide indiziert sind.) Es stellt sich heraus, dass wir keine solchen Tiere haben, da die Abfrage in etwa 300 Millisekunden eine leere Menge zurückgibt. Es wäre ungefähr doppelt so schnell gewesen, wenn ich '=' anstatt 'like' verwendet hätte, aber ich habe eine Vorahnung, die mir etwas Tipparbeit ersparen soll.

Es stellt sich heraus, dass der Hauptzookeeper denkt, er hätte einige der Schwäne als "schwärzlich" eingestuft, also ändere ich die Abfrage entsprechend:

select top 10 * 
from animal  
where colour like 'black%' 
and species like 'swan'

Es stellte sich heraus, dass es auch keine gibt (und tatsächlich gibt es keine 'schwarzen%' Tiere außer 'schwarzen'), aber die Abfrage dauert jetzt ungefähr 30 Sekunden, um leer zurückzukehren.

Es scheint, dass es nur die Kombination von 'top' und 'like%' ist, die Probleme verursacht, weil

select count(*) 
from animal  
where colour like 'black%' 
and species like 'swan'

Gibt sehr schnell und gerade 0 zurück

select * 
from animal 
where colour like 'black%' 
and species like 'swan'

kehrt im Bruchteil einer Sekunde leer zurück.

Hat jemand eine Idee, warum sich 'top' und '%' zusammenschließen sollten, um einen derart dramatischen Leistungsverlust zu verursachen, insbesondere in einer leeren Ergebnismenge?

BEARBEITEN: Nur zur Verdeutlichung, ich verwende keine FreeText-Indizes, ich meinte nur, dass die Felder zum Zeitpunkt der Eingabe Freitext sind, dh nicht in der Datenbank normalisiert. Entschuldigung für die Verwirrung, schlechte Formulierung meinerseits.

Antworten:


76

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.

Bildbeschreibung hier eingeben

Der Plan kann einen Merge-Join zwischen den Ergebnissen der beiden Indizes für die idSpalte 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 iddiesem 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 TOPIterator 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

Bildbeschreibung hier eingeben

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 9130werden weitere Informationen angezeigt

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan'       
OPTION (QUERYTRACEON 9130)  

Bildbeschreibung hier eingeben

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 TOPAnforderung 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

Bildbeschreibung hier eingeben

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.

TOPCI-Scan entfernen und erzwingen

SELECT *
FROM   animal WITH (INDEX = 1)
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Wird 88.0586auf 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 10Scan 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 TOPwird, 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)       

Bildbeschreibung hier eingeben

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 10Dies 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%'                         |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+

15

Als ich diese faszinierende Idee begründete, habe ich einige Nachforschungen angestellt und bin auf dieses Q / A gestoßen. Wie (und warum) wirkt sich TOP auf einen Ausführungsplan aus?

Grundsätzlich werden durch die Verwendung von TOP die Kosten der nachfolgenden Operatoren (auf nicht triviale Weise) geändert, wodurch sich auch der Gesamtplan ändert (es wäre großartig, wenn Sie ExecPlans mit und ohne TOP 10 einbeziehen würden), wodurch sich die Gesamtausführung von ziemlich stark ändert die Abfrage.

Hoffe das hilft.

Zum Beispiel habe ich es in einer Datenbank versucht und: -wenn kein top aufgerufen wird, wird Parallelität verwendet -wenn TOP verwendet wird, wird Parallelität nicht verwendet

Wenn Sie also Ihre Ausführungspläne anzeigen, erhalten Sie weitere Informationen.

Einen schönen Tag noch


-1

Ich glaube, dies liegt möglicherweise an der zugrunde liegenden Natur von MSSQL 2005 und der Art und Weise, wie das Abfrageoptimierungsprogramm entscheidet, welcher Ausführungsplan am effizientesten ist.

Wenn Sie eine SQL-Variable verwenden, sollte das Abfrageoptimierungsprogramm Hash-Übereinstimmungen anstelle von verschachtelten Schleifen verwenden, was zu einem viel höheren Grad an Parallelität führt.

Versuchen:

DECLARE @topn INT = 10
SELECT TOP (@topn) *
FROM    animal
WHERE   colour LIKE 'black%' 
AND species LIKE 'swan'

5
Wenn Sie den TOPWert in einer Variablen verschleiern, wird dies TOP 100eher als angenommen TOP 10. Dies kann hilfreich sein oder auch nicht, je nachdem, wie hoch der Wendepunkt zwischen den beiden Plänen ist.
Martin Smith
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.