it-swarm.com.de

Riesige Verlangsamung der SQL Server-Abfrage beim Hinzufügen eines Platzhalters (oder oben)

Ich habe einen Zoo mit 20 Millionen Tieren, den 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 Details über alle schwarzen Schwäne erhalten und wollte daher nicht die Ergebnisseite überfluten, die ich gemacht habe:

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

(Ja, diese Felder sind unabsichtlich Freetext, aber beide sind indiziert). Es stellt sich heraus, dass wir keine solchen Tiere haben, da die Abfrage in etwa 300 Millisekunden einen leeren Satz zurückgibt. Es wäre ungefähr doppelt so schnell gewesen, wenn ich '=' anstelle von 'like' verwendet hätte, aber ich habe eine Vorahnung, die mir das Tippen ersparen wird.

Es stellt sich heraus, dass der Tierpfleger glaubt, einige der Schwäne als "schwärzlich" eingegeben zu haben, daher ä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 dieser Tiere 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 nur die Kombination von 'top' und 'like%' Probleme verursacht, weil

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

gibt sehr schnell und gleichmäßig 0 zurück

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

kehrt in Sekundenbruchteilen leer zurück.

Hat jemand eine Idee, warum sich 'top' und '%' verschwören sollten, um einen solch dramatischen Leistungsverlust zu verursachen, insbesondere in einer leeren Ergebnismenge?

BEARBEITEN: Zur Verdeutlichung verwende ich keine FreeText-Indizes. Ich habe nur gemeint, dass die Felder zum Zeitpunkt des Eintrags Freetext sind, d. H. Nicht in der Datenbank normalisiert. Entschuldigung für die Verwirrung, schlechte Formulierung meinerseits.

52
stovroz

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, der bei 0.0563167 Berechnet wird.

enter image description here

Der Plan kann eine Zusammenführungsverbindung zwischen den Ergebnissen der beiden Indizes in der Spalte id durchführen. ( Weitere Details zum Merge-Join-Algorithmus hier ).

Beim Zusammenführen von Verknüpfungen müssen beide Eingaben nach dem Verknüpfungsschlüssel sortiert werden.

Die nicht gruppierten Indizes sind nach (species, id) Und (colour, id) Sortiert (nicht eindeutige nicht gruppierte Indizes immer muss der Zeilenlokator implizit am Ende des Schlüssels hinzugefügt werden wenn nicht explizit hinzugefügt ). Die Abfrage ohne Platzhalter führt eine Gleichstellungssuche in species = 'swan' Und colour ='black' Durch. Da bei jeder Suche nur ein exakter Wert aus der führenden Spalte abgerufen wird, werden die übereinstimmenden Zeilen nach id sortiert. Daher ist dieser Plan möglich.

Abfrageplanoperatoren von links nach rechts ausführen . 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 Iterator TOP fordert keine weiteren Zeilen mehr von seinem untergeordneten Element an, sobald 10 empfangen wurden.

SQL Server verfügt über Statistiken zu den Indizes, aus denen hervorgeht, dass 1% der Zeilen mit jedem Prädikat übereinstimmen. Es wird angenommen, dass diese Statistiken unabhängig sind (d. H. Weder positiv noch negativ korreliert sind), so dass im Durchschnitt, sobald sie 1.000 Zeilen verarbeitet haben, die mit dem ersten Prädikat übereinstimmen, 10 gefunden werden, die mit dem zweiten übereinstimmen, und beendet werden können. (Der obige Plan zeigt tatsächlich 987 statt 1.000, aber nah genug).

Da die Prädikate negativ korreliert sind, zeigt der tatsächliche Plan, dass alle 200.000 übereinstimmenden Zeilen von 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 Suche erforderlich war.

Vergleichen mit

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Welches gibt den Plan unten, der bei 0.567943 Berechnet wird.

enter image description here

Das Hinzufügen des nachfolgenden Platzhalters hat jetzt einen Index-Scan verursacht. Die Kosten für den Plan sind jedoch für einen Scan in einer 20-Millionen-Zeilentabelle immer noch recht niedrig.

Durch Hinzufügen von querytraceon 9130 Werden weitere Informationen angezeigt

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

enter image description here

Es ist ersichtlich, dass SQL Server davon ausgeht, dass nur etwa 100.000 Zeilen gescannt werden müssen, bevor 10 gefunden werden, die dem Prädikat entsprechen, und TOP die Anforderung von Zeilen beenden kann.

Dies ist wiederum mit der Annahme der Unabhängigkeit als 10 * 100 * 100 = 100,000 Sinn.

Lassen Sie uns abschließend 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

enter image description here

Der Hauptunterschied besteht darin, dass das Prädikat colour like 'black%' Jetzt mehreren verschiedenen Farben entsprechen kann. Dies bedeutet, dass nicht mehr garantiert wird, dass die übereinstimmenden Indexzeilen für dieses Prädikat in der Reihenfolge id sortiert sind.

Beispielsweise könnte die Indexsuche für 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, die IDs in verschiedenen Farben jedoch möglicherweise nicht.

Infolgedessen kann SQL Server keinen Schnittpunkt für Zusammenführungsverknüpfungsindizes mehr ausführen (ohne einen blockierenden Sortieroperator hinzuzufügen) und stattdessen einen Hash-Join ausführen. Hash Join blockiert die Build-Eingabe, sodass die Kosten nun 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.

Die Sondeneingabe ist jedoch nicht blockierend und es wird immer noch fälschlicherweise geschätzt, dass die Prüfung nach der Verarbeitung von 987 Zeilen beendet werden kann.

(Weitere Informationen zu nicht blockierenden oder blockierenden Iteratoren hier)

Angesichts der erhöhten Kosten für die zusätzlichen geschätzten Zeilen und den Hash-Join sieht der partielle Clustered-Index-Scan billiger 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 100.000, die beim Vergleich der Pläne angenommen wurden.

Wenn Sie den Wert von TOP erhöhen (oder vollständig entfernen), stößt Sie schließlich auf einen Wendepunkt, an dem die Anzahl der Zeilen, die der CI-Scan voraussichtlich abdecken muss, diesen Plan teurer erscheinen lässt und zum Indexschnittplan zurückkehrt . Für mich ist der Grenzwert zwischen den beiden Plänen TOP (89) vs TOP (90).

Für Sie kann es durchaus unterschiedlich sein, da es davon abhängt, wie breit der Clustered-Index ist.

Entfernen des TOP und Erzwingen des CI-Scans

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

Wird auf meinem Computer für meine Beispieltabelle mit 88.0586 Berechnet.

Wenn SQL Server wissen würde, dass der Zoo keine schwarzen Schwäne hat und dass ein vollständiger Scan durchgeführt werden muss, anstatt nur 100.000 Zeilen zu lesen, wird dieser Plan nicht ausgewählt.

Ich habe mehrspaltige Statistiken für animal(species,colour) und animal(colour,species) ausprobiert und Statistiken für animal (colour) where species = 'swan' gefiltert, aber keine davon hilft, sie davon zu überzeugen, dass schwarze Schwäne nicht existieren und die Der Scan TOP 10 Muss mehr als 100.000 Zeilen verarbeiten.

Dies liegt an der "Einschlussannahme", bei der SQL Server im Wesentlichen davon ausgeht, dass bei der Suche nach etwas wahrscheinlich etwas vorhanden ist.

Ab 2008+ gibt es ein dokumentiertes Trace-Flag 4138 , das die Zeilentore ausschaltet. Dies hat zur Folge, dass der Plan ohne die Annahme berechnet wird, dass TOP es den untergeordneten Operatoren ermöglicht, vorzeitig zu beenden, ohne alle übereinstimmenden Zeilen zu lesen. Mit diesem Trace-Flag erhalte ich natürlich den optimaleren Indexschnittplan.

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

enter image description here

Dieser Plan kostet jetzt korrekt das Lesen der gesamten 200.000 Zeilen in beiden Indexsuchen, kostet aber mehr als die Schlüsselsuche (geschätzte 2.000 gegenüber der tatsächlichen 0. Das TOP 10 Würde dies auf maximal 10 beschränken, aber das Trace-Flag verhindert dies dies wird berücksichtigt). Trotzdem ist der Plan deutlich günstiger als der vollständige CI-Scan und wird daher ausgewählt.

Natürlich ist dieser Plan möglicherweise nicht optimal für Kombinationen, die häufig sind. Wie weiße Schwäne.

Ein zusammengesetzter Index für animal (colour, species) oder idealerweise animal (species, colour) würde es ermöglichen, dass die Abfrage für beide Szenarien viel effizienter ist.

Um den zusammengesetzten Index möglichst effizient zu nutzen, müsste auch LIKE 'swan' In = 'swan' Geändert werden.

Die folgende Tabelle zeigt die Suchprädikate und Restprädikate, die in den Ausführungsplänen für alle vier Permutationen angezeigt werden.

+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
|                 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%'                         |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
76
Martin Smith

Als ich dieses faszinierende Thema fand, habe ich etwas gesucht und bin auf dieses Q/A gestoßen Wie (und warum) wirkt sich TOP auf einen Ausführungsplan aus?

Grundsätzlich ändert die Verwendung von TOP die Kosten der Betreiber, die darauf folgen (auf nicht triviale Weise), wodurch sich auch der Gesamtplan ändert (es wäre großartig, wenn Sie ExecPlans mit und ohne TOP 10 einbeziehen würden), was die Gesamtausführung von TOP 10 ziemlich verä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 - Mit TOP wird Parallelität nicht verwendet

Das Anzeigen Ihrer Ausführungspläne würde also wiederum weitere Informationen liefern.

Einen schönen Tag noch

15