SQL Server ma optymalizator oparty na kosztach, który wykorzystuje wiedzę o różnych tabelach biorących udział w zapytaniu, aby stworzyć najbardziej optymalny plan w czasie dostępnym podczas kompilacji. Ta wiedza obejmuje wszelkie istniejące indeksy i ich rozmiary oraz wszelkie statystyki kolumn. Częścią tego, co składa się na znalezienie optymalnego planu zapytania, jest próba zminimalizowania liczby fizycznych odczytów potrzebnych podczas wykonywania planu.
Kilka razy zostałem zapytany, dlaczego optymalizator nie bierze pod uwagę zawartości puli buforów SQL Server podczas kompilowania planu zapytań, ponieważ z pewnością może to przyspieszyć wykonanie zapytania. W tym poście wyjaśnię dlaczego.
Określanie zawartości puli buforów
Pierwszym powodem, dla którego optymalizator ignoruje pulę buforów, jest to, że ustalenie, co znajduje się w puli buforów, nie jest trywialnym problemem ze względu na sposób organizacji puli buforów. Strony plików danych są kontrolowane w puli buforów przez małe struktury danych zwane buforami, które śledzą takie rzeczy jak (lista niewyczerpująca):
- Identyfikator strony (numer pliku:numer-strony-w-pliku)
- Ostatnie odwołanie do strony (używane przez leniwego autora w celu zaimplementowania ostatnio używanego algorytmu, który w razie potrzeby tworzy wolną przestrzeń)
- Lokalizacja pamięci strony 8KB w puli buforów
- Czy strona jest brudna, czy nie (brudna strona zawiera na niej zmiany, które nie zostały jeszcze zapisane z powrotem w trwałej pamięci masowej)
- Jednostka alokacji, do której należy strona (wyjaśniona tutaj) oraz identyfikator jednostki alokacji mogą być użyte do ustalenia, do jakiej tabeli i indeksu strona jest częścią
Dla każdej bazy danych, która zawiera strony w puli buforów, istnieje lista skrótów stron, w kolejności według identyfikatorów stron, którą można szybko przeszukać w celu ustalenia, czy strona jest już w pamięci, czy też należy wykonać fizyczny odczyt. Jednak nic nie pozwala łatwo programowi SQL Server określić, jaki procent poziomu liścia dla każdego indeksu tabeli znajduje się już w pamięci. Kod musiałby przeskanować całą listę buforów dla bazy danych, szukając buforów, które mapują strony dla danej jednostki alokacji. A im więcej stron w pamięci dla bazy danych, tym dłużej zajmie skanowanie. Byłoby to zbyt drogie w ramach kompilacji zapytań.
Jeśli jesteś zainteresowany, napisałem jakiś czas temu post z kodem T-SQL, który skanuje pulę buforów i podaje pewne metryki, używając DMV sys.dm_os_buffer_descriptors .
Dlaczego używanie zawartości puli buforów byłoby niebezpieczne
Załóżmy, że *istnieje* wysoce wydajny mechanizm określania zawartości puli buforów, którego może użyć optymalizator, aby wybrać indeks, który ma być użyty w planie zapytania. Hipoteza, którą zamierzam zbadać, jest taka, że jeśli optymalizator wie wystarczająco dużo o mniej wydajnym (większym) indeksie jest już w pamięci, w porównaniu z najbardziej wydajnym (mniejszym) indeksem do użycia, powinien wybrać indeks w pamięci, ponieważ zmniejsz liczbę wymaganych fizycznych odczytów, a zapytanie będzie działać szybciej.
Scenariusz, którego zamierzam użyć, jest następujący:tabela BigTable ma dwa nieklastrowane indeksy, Index_A i Index_B, które całkowicie pokrywają dane zapytanie. Zapytanie wymaga pełnego skanowania poziomu liścia indeksu w celu pobrania wyników zapytania. Tabela ma 1 milion wierszy. Index_A ma 200 000 stron na poziomie liścia, a Index_B ma 1 milion stron na poziomie liścia, więc pełne skanowanie Index_B wymaga przetworzenia pięć razy więcej stron.
Stworzyłem ten wymyślny przykład na laptopie z SQL Server 2019 z 8 rdzeniami procesora, 32 GB pamięci i dyskami półprzewodnikowymi. Kod wygląda następująco:
CREATE TABLE BigTable ( C1 BIGINT IDENTITY, c2 AS (c1 * 2), c3 CHAR (1500) DEFAULT 'a', c4 CHAR (5000) DEFAULT 'b');GO WSTAW DO BigTable DOMYŚLNE WARTOŚCI;GO 1000000 UTWÓRZ INDEKS NIESKLASTRAROWANY Index_A ON BigTable (c2) INCLUDE (c3);-- 5 rekordów na stronę =200 000 stron PRZEJDŹ UTWÓRZ INDEKS NIESKLASTRAROWANY Index_B ON BigTable (c2) INCLUDE (c4);-- 1 rekord na stronę =1 mln stron PRZEJDŹ /pre>A potem zmierzyłem czas wymyślonych zapytań:
DBCC DROPCLEANBUFFERS;GO -- Index_A nie znajduje się w pamięciSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));GO-- Czas procesora =796 ms, czas, który upłynął =764 ms -- Index_A w pamięciSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));GO-- Czas procesora =312 ms, czas, który upłynął =52 ms DBCC DROPCLEANBUFFERS;GO -- Index_B nie znajduje się w pamięciSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B));GO- - Czas CPU =2952 ms, czas trwania =2761 ms -- Index_B w pamięci SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B));GO-- Czas CPU =1219 ms, czas trwania =149 msMożesz zobaczyć, kiedy żaden z indeksów nie znajduje się w pamięci, Index_A jest z łatwością najbardziej wydajnym indeksem do użycia, z czasem zapytania wynoszącym 764 ms w porównaniu z 2761 ms przy użyciu Index_B, i to samo jest prawdą, gdy oba indeksy znajdują się w pamięci. Jeśli jednak Index_B znajduje się w pamięci, a Index_A nie, jeśli zapytanie używa Index_B (149 ms), będzie działać szybciej niż w przypadku użycia Index_A (764 ms).
Teraz pozwólmy optymalizatorowi oprzeć wybór planu na tym, co znajduje się w puli buforów…
Jeśli Index_A w większości nie znajduje się w pamięci, a Index_B znajduje się głównie w pamięci, bardziej wydajne byłoby skompilowanie planu zapytania, aby użyć Index_B dla zapytania uruchomionego w tej chwili. Mimo że Index_B jest większy i wymagałby większej liczby cykli procesora do skanowania, fizyczne odczyty są znacznie wolniejsze niż dodatkowe cykle procesora, więc bardziej wydajny plan zapytań minimalizuje liczbę fizycznych odczytów.
Ten argument obowiązuje tylko, a plan zapytań „use Index_B” jest tylko bardziej wydajny niż plan zapytań „use Index_A”, jeśli Index_B pozostaje głównie w pamięci, a Index_A pozostaje w większości w pamięci. Gdy tylko większość Index_A znajdzie się w pamięci, plan zapytań „use Index_A” będzie bardziej wydajny, a plan zapytań „use Index_B” jest złym wyborem.
Sytuacje, w których skompilowany plan „użyj indeksu_B” jest mniej wydajny niż oparty na kosztach plan „użyj indeksu_A” (uogólniając):
- Index_A i Index_B znajdują się w pamięci:skompilowany plan zajmie prawie trzy razy dłużej
- Żaden z indeksów nie rezyduje w pamięci:skompilowany plan zajmuje 3,5 razy dłużej
- Index_A rezyduje w pamięci, a Index_B nie:wszystkie fizyczne odczyty wykonywane przez plan są obce i zajmie to aż 53 razy dłużej
Podsumowanie
Chociaż w naszym ćwiczeniu myślowym optymalizator może wykorzystać wiedzę na temat puli buforów do skompilowania najbardziej wydajnego zapytania w jednej chwili, byłby to niebezpieczny sposób na kierowanie kompilacją planu ze względu na potencjalną zmienność zawartości puli buforów, co sprawia, że przyszła wydajność plan w pamięci podręcznej jest wysoce niewiarygodny.
Pamiętaj, że zadaniem optymalizatora jest szybkie znalezienie dobrego planu, niekoniecznie jednego najlepszego planu dla 100% wszystkich sytuacji. Moim zdaniem optymalizator SQL Server postępuje właściwie, ignorując rzeczywistą zawartość puli buforów SQL Server i zamiast tego polega na różnych regułach wyceny, aby stworzyć plan zapytań, który prawdopodobnie będzie najbardziej wydajny przez większość czasu .