Jest to decyzja optymalizatora kosztowego.
Szacunkowe koszty użyte w tym wyborze są nieprawidłowe, ponieważ zakłada statystyczną niezależność między wartościami w różnych kolumnach.
Jest podobny do problemu opisanego w Row Goals Gone Rogue, w którym liczby parzyste i nieparzyste są ujemnie skorelowane.
Jest łatwy do odtworzenia.
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);
Spróbuj teraz
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black'
AND species LIKE 'swan'
Daje to plan, poniżej którego kosztuje 0.0563167
.
Plan jest w stanie wykonać łączenie scalające między wynikami dwóch indeksów w id
kolumnie. ( Więcej szczegółów na temat algorytmu łączenia scalającego tutaj ).
Scal sprzężenie wymaga, aby oba dane wejściowe były uporządkowane według klucza łączenia.
Indeksy nieklastrowane są uporządkowane według (species, id)
i (colour, id)
odpowiednio (w nieunikalnych indeksach nieklastrowanych zawsze lokalizator wierszy jest zawsze dodawany na końcu klucza niejawnie, jeśli nie jest jawnie dodawany). Zapytanie bez symboli wieloznacznych wykonuje wyszukiwanie równości do species = 'swan'
i colour ='black'
. Ponieważ każde wyszukiwanie pobiera tylko jedną dokładną wartość z kolumny wiodącej, pasujące wiersze zostaną uporządkowane według, id
dlatego ten plan jest możliwy.
Operatorzy planu zapytań wykonują od lewej do prawej . Lewy operator żąda wierszy od swoich potomków, które z kolei żądają wierszy od swoich potomków (i tak dalej, aż dojdą do węzłów liści). TOP
Iterator zatrzyma zainteresowanie kolejnych wierszy z jej dziecko po raz 10 zostały otrzymane.
SQL Server ma statystyki dotyczące indeksów, które mówią, że 1% wierszy pasuje do każdego predykatu. Zakłada się, że statystyki te są niezależne (tzn. Nie są skorelowane ani dodatnio, ani ujemnie), więc średnio po przetworzeniu 1000 wierszy pasujących do pierwszego predykatu znajdzie 10 pasujących do drugiego i może wyjść. (powyższy plan pokazuje 987 zamiast 1000, ale wystarczająco blisko).
W rzeczywistości, ponieważ predykaty są ujemnie skorelowane, rzeczywisty plan pokazuje, że wszystkie 200 000 pasujących wierszy wymagało przetworzenia z każdego indeksu, ale jest to w pewnym stopniu złagodzone, ponieważ zero połączone wiersze oznacza również, że faktycznie potrzebne były zerowe wyszukiwania.
Porównać z
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
Co daje plan, poniżej którego kosztuje 0.567943
Dodanie końcowej karty wieloznacznej spowodowało teraz skanowanie indeksu. Koszt planu jest jednak nadal dość niski w przypadku skanowania tabeli z 20 milionami wierszy.
Dodanie querytraceon 9130
pokazuje więcej informacji
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
OPTION (QUERYTRACEON 9130)
Można zauważyć, że SQL Server uważa, że będzie musiał skanować tylko około 100 000 wierszy, zanim znajdzie 10 pasujących predykatów i TOP
może przestać żądać wierszy.
Ponownie ma to sens przy założeniu niezależności jako 10 * 100 * 100 = 100,000
Wreszcie spróbujmy wymusić plan przecięcia indeksu
SELECT TOP 10 *
FROM animal WITH (INDEX(ix_species), INDEX(ix_colour))
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
Daje mi to równoległy plan z szacowanym kosztem 3,4625
Główną różnicą jest to, że colour like 'black%'
predykat może teraz pasować do wielu różnych kolorów. Oznacza to, że nie można już zagwarantować, że pasujące wiersze indeksu dla tego predykatu będą sortowane w kolejności od id
.
Na przykład wyszukiwanie indeksu like 'black%'
może zwrócić następujące wiersze
+------------+----+
| Colour | id |
+------------+----+
| black | 12 |
| black | 20 |
| black | 23 |
| black | 25 |
| blackberry | 1 |
| blackberry | 50 |
+------------+----+
W obrębie każdego koloru identyfikatory są uporządkowane, ale identyfikatory w różnych kolorach mogą nie być.
W rezultacie SQL Server nie może już wykonywać przecięcia indeksu łączenia scalającego (bez dodawania blokującego operatora sortowania) i zamiast tego wybiera łączenie mieszające. Łączenie hash blokuje dane wejściowe kompilacji, więc teraz koszt odzwierciedla fakt, że wszystkie pasujące wiersze będą musiały zostać przetworzone z danych wejściowych kompilacji, zamiast zakładać, że będzie musiał skanować tylko 1000, jak w pierwszym planie.
Dane wejściowe sondy nie są jednak blokowane i nadal niepoprawnie szacuje, że będzie w stanie zatrzymać sondowanie po przetworzeniu 987 wierszy od tego.
(Więcej informacji na temat nieblokowania vs. blokowania iteratorów tutaj)
Biorąc pod uwagę zwiększone koszty dodatkowych szacowanych wierszy i skrótu, częściowe skanowanie indeksu klastrowego wygląda taniej.
W praktyce oczywiście „częściowy” skan indeksu klastrowego wcale nie jest częściowy i musi przeszukiwać całe 20 milionów wierszy, a nie 100 tysięcy założonych podczas porównywania planów.
Zwiększenie wartości TOP
(lub całkowite usunięcie) ostatecznie napotyka punkt krytyczny, w którym liczba wierszy, które według szacunków będzie musiał pokryć skan CI, powoduje, że plan wygląda na droższy i wraca do planu przecięcia indeksu. Dla mnie punktem odcięcia pomiędzy dwoma planami jest TOP (89)
kontra TOP (90)
.
Dla ciebie może się różnić, ponieważ zależy od tego, jak szeroki jest indeks klastrowany.
Usuwanie TOP
i wymuszanie skanowania CI
SELECT *
FROM animal WITH (INDEX = 1)
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
Koszt w 88.0586
mojej maszynie kosztuje mój przykładowy stół.
Jeśli SQL Server byłby świadomy, że zoo nie ma czarnych łabędzi i że będzie musiał wykonać pełne skanowanie, a nie tylko odczytać 100 000 wierszy, ten plan nie zostałby wybrany.
Próbowałem wielo statystyk kolumnowej na animal(species,colour)
i animal(colour,species)
i sączy się na statystyki animal (colour) where species = 'swan'
, ale żaden z nich nie pomoże przekonać go, że czarne łabędzie nie istnieją i TOP 10
skanowania będą musiały przetwarzać ponad 100.000 wierszy.
Wynika to z „założenia włączenia”, w którym SQL Server zasadniczo zakłada, że jeśli szukasz czegoś, prawdopodobnie istnieje.
W wersji 2008+ dostępna jest udokumentowana flaga śledzenia 4138, która wyłącza cele rzędu. Skutkuje to tym, że plan jest kosztowany bez założenia, że TOP
pozwoli operatorom potomnym na wcześniejsze zakończenie bez odczytywania wszystkich pasujących wierszy. Z tą flagą śledzenia w naturalny sposób otrzymuję bardziej optymalny plan przecięcia indeksu.
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
OPTION (QUERYTRACEON 4138)
Plan ten teraz poprawnie kosztuje za odczyt pełnych 200 tysięcy wierszy w obu poszukiwaniach indeksu, ale przekracza koszty kluczowych wyszukiwań (szacunkowo 2 tysiące w porównaniu do rzeczywistego 0. TOP 10
Ograniczałoby to do maksymalnie 10, ale flaga śledzenia nie pozwala na wzięcie tego pod uwagę) . Mimo to plan jest znacznie tańszy niż pełny skan CI, więc wybrano go.
Oczywiście plan ten może nie być optymalna dla kombinacji, które są wspólne. Takie jak białe łabędzie.
Indeks złożony na animal (colour, species)
lub idealnie animal (species, colour)
pozwoliłby, aby zapytanie było znacznie bardziej wydajne dla obu scenariuszy.
Aby jak najefektywniej wykorzystać indeks złożony LIKE 'swan'
, należy go również zmienić na = 'swan'
.
Poniższa tabela pokazuje predykaty wyszukiwania i pozostałe predykaty pokazane w planach wykonania dla wszystkich czterech permutacji.
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
| 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
wartości w zmiennej oznacza, że będzie ona przyjmować,TOP 100
a nieTOP 10
. To może, ale nie musi pomóc, w zależności od tego, jaki jest punkt krytyczny między dwoma planami.