Są funkcje, których wielu z nas unika, takie jak kursory, wyzwalacze i dynamiczny SQL. Nie ma wątpliwości, że każdy z nich ma swoje przypadki użycia, ale kiedy widzimy wyzwalacz z kursorem w dynamicznym SQL, może nas to wywołać (potrójny whammy).
Przewodniki po planach i sp_prepare są na podobnej łódce:gdybyś zobaczył, że używam jednego z nich, uniósłbyś brew; gdybyś zobaczył, że używam ich razem, prawdopodobnie sprawdziłbyś moją temperaturę. Ale, podobnie jak w przypadku kursorów, wyzwalaczy i dynamicznego SQL, mają swoje przypadki użycia. Niedawno natknąłem się na scenariusz, w którym używanie ich razem było korzystne.
Tło
Mamy dużo danych. I wiele aplikacji działających na tych danych. Niektóre z tych aplikacji są trudne lub niemożliwe do zmiany, w szczególności aplikacje gotowe od osób trzecich. Tak więc, gdy ich skompilowana aplikacja wysyła zapytania ad hoc do SQL Server, szczególnie w postaci przygotowanej instrukcji, i gdy nie mamy swobody dodawania lub zmiany indeksów, kilka możliwości dostrajania jest natychmiast niedostępnych.
W tym przypadku mieliśmy tabelę z kilkoma milionami wierszy. Uproszczona i oczyszczona wersja:
CREATE TABLE dbo.TheThings ( ThingID bigint NOT NULL, TypeID uniqueidentifier NOT NULL, dt1 datetime NOT NULL DEFAULT sysutcdatetime(), dt2 datetime NOT NULL DEFAULT sysutcdatetime(), dt3 datetime NOT NULL DEFAULT sysutcdatetime(), CONSTRAINT PK_TheThings PRIMARY KEY (ThingID) ); CREATE INDEX ix_type ON dbo.TheThings(TypeID); SET NOCOUNT ON; GO DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4', @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1000) 1000 + ROW_NUMBER() OVER (ORDER BY name), @guid1 FROM sys.all_columns; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1) 2500, @guid2 FROM sys.all_columns; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1000) 3000 + ROW_NUMBER() OVER (ORDER BY name), @guid1 FROM sys.all_columns;
Przygotowane oświadczenie z aplikacji wyglądało tak (jak widać w pamięci podręcznej planu):
(@P0 varchar(8000))SELECT * FROM dbo.TheThings WHERE TypeID = @P0
Problem polega na tym, że dla niektórych wartości TypeID
, byłoby wiele tysięcy wierszy. W przypadku innych wartości będzie ich mniej niż 10. Jeśli na podstawie jednego typu parametru zostanie wybrany (i ponownie wykorzystany) niewłaściwy plan, może to stanowić problem dla innych. W przypadku zapytania, które pobiera kilka wierszy, chcemy, aby wyszukiwanie indeksu z wyszukiwaniami pobierało dodatkowe niepokryte kolumny, ale w przypadku zapytania zwracającego 700 tys. wierszy chcemy po prostu skanować indeks klastrowy. (Idealnie, indeks obejmowałby, ale tej opcji nie było tym razem na kartach).
W praktyce aplikacja zawsze otrzymywała wariację skanowania, mimo że była to ta, która była potrzebna w około 1% przypadków. 99% zapytań korzystało ze skanowania 2 milionów wierszy, podczas gdy mogły użyć wyszukiwania + 4 lub 5 wyszukiwań.
Możemy łatwo odtworzyć to w Management Studio, uruchamiając to zapytanie:
DBCC FREEPROCCACHE; DECLARE @P0 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; SELECT * FROM dbo.TheThings WHERE TypeID = @P0; GO DBCC FREEPROCCACHE; DECLARE @P0 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; SELECT * FROM dbo.TheThings WHERE TypeID = @P0; GO
Plany wróciły tak:
Oszacowanie w obu przypadkach wynosiło 1000 wierszy; ostrzeżenia po prawej są spowodowane pozostałościami we/wy.
Jak możemy upewnić się, że zapytanie dokonało właściwego wyboru w zależności od parametru? Musielibyśmy dokonać ponownej kompilacji bez dodawania wskazówek do zapytania, włączania flag śledzenia lub zmiany ustawień bazy danych.
Jeśli uruchomiłem zapytania niezależnie za pomocą OPTION (RECOMPILE)
, w razie potrzeby otrzymam zapytanie:
DBCC FREEPROCCACHE; DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4', @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; SELECT * FROM dbo.TheThings WHERE TypeID = @guid1 OPTION (RECOMPILE); SELECT * FROM dbo.TheThings WHERE TypeID = @guid2 OPTION (RECOMPILE);
Dzięki RECOMPILE otrzymujemy dokładniejsze oszacowania i szukamy, kiedy ich potrzebujemy.
Ale znowu nie mogliśmy bezpośrednio dodać wskazówki do zapytania.
Wypróbujmy przewodnik po planie
Wiele osób ostrzega przed przewodnikami po planach, ale byliśmy tu trochę w kącie. Zdecydowanie wolelibyśmy zmienić zapytanie lub indeksy, gdybyśmy mogli. Ale to może być kolejna najlepsza rzecz.
EXEC sys.sp_create_plan_guide @name = N'TheThingGuide', @stmt = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', @type = N'SQL', @params = N'@P0 varchar(8000)', @hints = N'OPTION (RECOMPILE)';
Wydaje się proste; testowanie to jest problem. Jak symulujemy przygotowane zestawienie w Management Studio? Jak możemy mieć pewność, że aplikacja otrzymuje plan z przewodnikiem i że jest to wyraźnie spowodowane przewodnikiem po planie?
Jeśli spróbujemy zasymulować to zapytanie w SSMS, zostanie to potraktowane jako oświadczenie ad hoc, a nie przygotowane oświadczenie, i nie mogłem tego uzyskać, aby pobrać przewodnik po planie:
DECLARE @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- also tried uniqueidentifier SELECT * FROM dbo.TheThings WHERE TypeID = @P0
Dynamiczny SQL również nie działał (to również zostało potraktowane jako instrukcja ad hoc):
DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', @params nvarchar(max) = N'@P0 varchar(8000)', -- also tried uniqueidentifier @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; EXEC sys.sp_executesql @sql, @params, @P0;
A nie mogłem tego zrobić, bo to też nie podniosłoby przewodnika po planie (tu przejmuje parametryzacja, a ja nie miałem swobody w zmianie ustawień bazy danych, nawet jeśli miałoby to być traktowane jak przygotowana wypowiedź) :
SELECT * FROM TheThings WHERE TypeID = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
Nie mogę sprawdzić pamięci podręcznej planu dla zapytań uruchomionych z aplikacji, ponieważ buforowany plan nie wskazuje nic na temat użycia przewodnika planu (SSMS wstrzykuje te informacje do pliku XML podczas generowania rzeczywistego planu). A jeśli zapytanie naprawdę zawiera wskazówkę dotyczącą RECOMPILE, którą przekazuję w przewodniku po planie, to w jaki sposób mógłbym kiedykolwiek zobaczyć jakiekolwiek dowody w pamięci podręcznej planu?
Spróbujmy sp_prepare
W swojej karierze używałem sp_prepare rzadziej niż przewodników po planach i nie zalecałbym używania go do kodu aplikacji. (Jak wskazuje Erik Darling, oszacowanie można wyciągnąć z wektora gęstości, a nie z wąchania parametru.)
W moim przypadku nie chcę go używać ze względu na wydajność, chcę go użyć (wraz z sp_execute) do symulacji przygotowanej instrukcji pochodzącej z aplikacji.
DECLARE @o int; EXEC sys.sp_prepare @o OUTPUT, N'@P0 varchar(8000)', N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0'; EXEC sys.sp_execute @o, 'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; -- PK scan EXEC sys.sp_execute @o, 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- IX seek + lookup
SSMS pokazuje nam, że przewodnik po planie był używany w obu przypadkach.
Nie będziesz mógł sprawdzić pamięci podręcznej planu pod kątem tych wyników z powodu ponownej kompilacji. Ale w scenariuszu takim jak mój, powinieneś być w stanie zobaczyć efekty w monitorowaniu, jawnym sprawdzaniu za pomocą zdarzeń rozszerzonych lub obserwowaniu złagodzenia symptomu, który skłonił Cię do zbadania tego zapytania w pierwszej kolejności (należy pamiętać, że średni czas działania, zapytanie na statystyki itp. może mieć wpływ dodatkowa kompilacja).
Wniosek
Był to jeden przypadek, w którym przewodnik planu był korzystny, a sp_prepare był przydatny w sprawdzaniu, czy będzie działał dla aplikacji. Te nieczęsto się przydają i rzadziej razem, ale dla mnie było to ciekawe połączenie. Nawet bez przewodnika po planie, jeśli chcesz używać programu SSMS do symulowania aplikacji wysyłającej przygotowane zestawienia, sp_prepare jest twoim przyjacielem. (Zobacz także sp_prepexec, który może być skrótem, jeśli nie próbujesz zweryfikować dwóch różnych planów dla tego samego zapytania.)
Zauważ, że to ćwiczenie niekoniecznie miało na celu uzyskanie lepszych wyników przez cały czas – miało to na celu spłaszczenie wariancji wydajności. Ponowne kompilacje oczywiście nie są darmowe, ale zapłacę niewielką karę, aby 99% moich zapytań zostało wykonanych w ciągu 250 ms, a 1% w ciągu 5 sekund, zamiast utknąć z planem, który jest absolutnie okropny dla 99% zapytań lub 1% zapytań.