Jednym z dostępnych algorytmów łączenia dwóch tabel w SQL Server są pętle zagnieżdżone. Łączenie zagnieżdżonych pętli używa jednego wejścia łączenia jako zewnętrznej tabeli wejściowej i jednego jako wewnętrznej tabeli wejściowej. Zewnętrzna pętla iteruje wiersz po wierszu zewnętrznej tabeli wejściowej. Wewnętrzna pętla, wykonywana dla każdego zewnętrznego wiersza, szuka pasujących wierszy w wewnętrznej tabeli wejściowej.
Nazywa się to naiwnym połączeniem zagnieżdżonych pętli.
Jeśli masz indeks warunków złączenia w wewnętrznej tabeli wejściowej, nie jest konieczne wykonywanie wewnętrznej pętli dla każdego wiersza tabeli zewnętrznej. Zamiast tego możesz przekazać wartość z tabeli zewnętrznej jako argument wyszukiwania i połączyć wszystkie zwrócone wiersze tabeli wewnętrznej z wierszami tabeli zewnętrznej.
Wyszukiwanie według tabeli wewnętrznej ma dostęp losowy. SQL Server, począwszy od wersji 2005, ma optymalizację sortowania wsadowego (nie mylić z operatorem Sort w trybie wsadowym dla indeksów magazynu kolumn). Celem optymalizacji jest uporządkowanie kluczy wyszukiwania z tabeli zewnętrznej przed pobraniem danych z tabeli wewnętrznej. W ten sposób dostęp losowy będzie sekwencyjny.
Plan wykonania nie wyświetla operacji sortowania wsadowego jako oddzielnego operatora. Zamiast tego możesz zobaczyć właściwość Optimized=true w operatorze Nested Loops. Gdyby można było zobaczyć sortowanie wsadowe jako oddzielny operator w planie, wyglądałoby to następująco:
W tym pseudo planie odczytujemy dane z nieklastrowanego indeksu ix_CustomerID w kolejności tego klucza indeksu CustomerID. Następnie musimy wykonać Key Lookup w indeksie klastrowym, ponieważ ix_CustomerID nie jest indeksem pokrywającym. Key Lookup to operacja wyszukiwania kluczy w indeksie klastrowym — dostęp losowy. Aby uczynić to sekwencyjnym, SQL Server może wykonać sortowanie wsadowe według klucza indeksu klastrowego.
Aby dowiedzieć się więcej o sortowaniu wsadowym, zapoznaj się z moim artykułem Sortowanie wsadowe i zagnieżdżone pętle.
Ta optymalizacja zapewnia duży wzrost przy wystarczającej liczbie wierszy. Więcej o wynikach testów można przeczytać na blogu OPTIMIZED Nested Loops Joins, stworzonym przez Craiga Freedmana, programistę optymalizacji.
Jeśli jednak rzeczywista liczba wierszy jest mniejsza niż oczekiwana, dodatkowe koszty procesora związane z budowaniem tego rodzaju mogą ukryć korzyści, zwiększyć zużycie procesora i zmniejszyć jego wydajność.
Rozważ ten konkretny przykład:
use tempdb; go -- create a test table (SalesOrderID - clustered PK) create table dbo.SalesOrder(SalesOrderID int identity primary key, CustomerID int not null, SomeData char(200) not null); go -- add test data with n as (select top(1000000) rn = row_number() over(order by (select null)) from sys.all_columns c1,sys.all_columns c2) insert dbo.SalesOrder(CustomerID, SomeData) select rn%500000, str(rn,100) from n; -- create a clustered index create index ix_c on dbo.Salesorder(CustomerID); go -- the batch sort optimization is enabled by default (Nested Loops: Optimized = true) select * from dbo.SalesOrder with(index(ix_c)) where CustomerID < 1000; -- disable it with the DISABLE_OPTIMIZED_NESTED_LOOP hint (Nested Loops: Optimized = false) select * from dbo.SalesOrder with(index(ix_c)) where CustomerID < 1000 option(use hint('DISABLE_OPTIMIZED_NESTED_LOOP')); go
Zwrócony wynik:
Chciałbym zwrócić uwagę na inną kolejność wierszy na wyjściu. Serwer zwraca wiersze w kolejności, w jakiej je przetwarza, ponieważ nie określiliśmy wyraźnie ORDER BY. W pierwszym przypadku stopniowo odczytujemy indeks ix_c. Jednak w celu zoptymalizowania losowych odczytów z indeksu klastrowanego filtrujemy wiersze według klastrowanego klucza indeksu SalesOrderID. W drugim przypadku nie ma sortowania, a odczyty są wykonywane w kolejności klucza CustomerID indeksu nieklastrowanego ix_c.
Różnica od flagi śledzenia 2340
Pomimo faktu, że dokumentacja określa flagę śledzenia 2340 jako odpowiednik wskazówki DISABLE_OPTIMIZED_NESTED_LOOP, w rzeczywistości nie jest to prawda.
Rozważmy następujący przykład, w którym użyję nieudokumentowanego polecenia UPDATE STATISTICS … WITH PAGECOUNT, aby oszukać optymalizator, mówiąc, że tabela zajmuje więcej stron niż w rzeczywistości. Następnie spójrz na te zapytania:
- Bez żadnych wskazówek (dodałem MAXDOP, aby zachować prosty plan);
- Z podpowiedzią DISABLE_OPTIMIZED_NESTED_LOOP;
- Z flagą śledzenia 2340.
-- create a huge table update statistics dbo.SalesOrder with pagecount = 100000; go set showplan_xml on; go -- 1. without hints select * from dbo.SalesOrder with(index(ix_c)) where CustomerID < 1000000 option(maxdop 1); -- 2. hint select * from dbo.SalesOrder with(index(ix_c)) where CustomerID < 1000000 option(use hint('DISABLE_OPTIMIZED_NESTED_LOOP'), maxdop 1); -- 3. trace flag select * from dbo.SalesOrder with(index(ix_c)) where CustomerID < 1000000 option(querytraceon 2340, maxdop 1); go set showplan_xml off; go
W rezultacie mamy następujące plany:
Zagnieżdżone pętle mają właściwość Optimized =false we wszystkich trzech planach. Faktem jest, że zwiększając szerokość tabeli, zwiększamy również koszt dostępu do danych. Gdy koszt jest wystarczająco wysoki, SQL Server może używać jawnego operatora sortowania zamiast niejawnego operatora sortowania wsadowego. Widzimy to w pierwszym planie zapytań.
W drugim zapytaniu użyliśmy wskazówki DISABLE_OPTIMIZED_NESTED_LOOP, która wyłącza niejawne sortowanie wsadowe. Usuwa jednak jawne sortowanie według oddzielnego operatora.
W trzecim planie widzimy, że pomimo dodania flagi śledzenia 2340 istnieje operator sortowania.
Zatem różnica między podpowiedzią a flagą jest następująca:podpowiedź wyłącza optymalizację poprzez przekształcenie dostępu losowego na szeregowy w zależności od tego, czy serwer zaimplementuje go z niejawnym sortowaniem wsadowym, czy z osobnym operatorem Sort.
PS Plany mogą zależeć od wyposażenia. Dlatego jeśli nie uda Ci się uruchomić tych zapytań, spróbuj zwiększyć lub zmniejszyć rozmiar kolumny SomeData char(200) w opisie tabeli dbo.SalesOrder.
Artykuł został przetłumaczony przez zespół Codingsight za zgodą autora.