To jedna z tych religijno-politycznych debat, które toczą się od lat:czy powinienem używać procedur składowanych, czy też powinienem umieszczać zapytania ad hoc w mojej aplikacji? Zawsze byłem zwolennikiem procedur składowanych z kilku powodów:
- Nie mogę zaimplementować ochrony przed wstrzyknięciem SQL, jeśli zapytanie jest skonstruowane w kodzie aplikacji. Deweloperzy mogą być świadomi sparametryzowanych zapytań, ale nic nie zmusza ich do prawidłowego ich używania.
- Nie mogę dostroić zapytania, które jest osadzone w kodzie źródłowym aplikacji, ani nie mogę wymusić żadnych najlepszych praktyk.
- Jeśli znajdę możliwość dostrojenia zapytań, aby je wdrożyć, muszę ponownie skompilować i ponownie wdrożyć kod aplikacji, a nie tylko zmienić procedurę składowaną.
- Jeśli zapytanie jest używane w wielu miejscach w aplikacji lub w wielu aplikacjach i wymaga zmiany, muszę je zmienić w wielu miejscach, podczas gdy w przypadku procedury składowanej muszę ją zmienić tylko raz (problemy z wdrożeniem na bok).
Widzę też, że wiele osób porzuca procedury składowane na rzecz ORM-ów. W przypadku prostych aplikacji prawdopodobnie będzie to w porządku, ale gdy Twoja aplikacja stanie się bardziej złożona, prawdopodobnie okaże się, że wybrany przez Ciebie ORM po prostu nie jest w stanie wykonać pewnych wzorców zapytań, *zmuszając* Cię do użycia procedury składowanej. Oznacza to, że obsługuje procedury składowane.
Chociaż nadal uważam, że wszystkie te argumenty są dość przekonujące, nie są one tym, o czym chcę dzisiaj mówić; Chcę porozmawiać o wydajności.
Wiele argumentów mówi po prostu:„procedury przechowywane działają lepiej!” W pewnym momencie mogło to być choć trochę prawdziwe, ale ponieważ SQL Server dodał możliwość kompilacji na poziomie instrukcji, a nie na poziomie obiektu, i zyskał potężną funkcjonalność, taką jak optimize for ad hoc workloads
, nie jest to już bardzo mocny argument. Dostrajanie indeksu i rozsądne wzorce zapytań mają znacznie większy wpływ na wydajność niż kiedykolwiek będzie miało zastosowanie procedura składowana; w nowoczesnych wersjach wątpię, czy znajdziesz wiele przypadków, w których dokładnie to samo zapytanie wykazuje zauważalne różnice w wydajności, chyba że wprowadzasz również inne zmienne (takie jak uruchamianie procedury lokalnie w porównaniu z aplikacją w innym centrum danych na innym kontynencie).
To powiedziawszy, istnieje aspekt wydajności, który często jest pomijany w przypadku zapytań ad hoc:pamięć podręczna planu. Możemy użyć optimize for ad hoc workloads
aby zapobiec zapełnianiu naszej pamięci podręcznej przez plany jednorazowego użytku (Kimberly Tripp (@KimberlyLTripp) z SQLskills.com ma tutaj świetne informacje na ten temat), co wpływa na plany jednorazowego użytku niezależnie od tego, czy zapytania są uruchamiane z poziomu procedury składowanej lub są prowadzone ad hoc. Innym wpływem, którego możesz nie zauważyć, niezależnie od tego ustawienia, jest sytuacja, gdy identyczny plany zajmują wiele slotów w pamięci podręcznej z powodu różnic w SET
opcje lub drobne delty w tekście zapytania. Całe zjawisko "wolno w aplikacji, szybko w SSMS" pomogło wielu ludziom rozwiązać problemy związane z ustawieniami takimi jak SET ARITHABORT
. Dzisiaj chciałem porozmawiać o różnicach w tekście zapytania i zademonstrować coś, co zaskakuje ludzi za każdym razem, gdy o tym wspominam.
Pamięć podręczna do wypalenia
Załóżmy, że mamy bardzo prosty system z AdventureWorks2012. Aby udowodnić, że to nie pomaga, włączyliśmy optimize for ad hoc workloads
:
EXEC sp_configure 'show advanced options', 1; GO RECONFIGURE WITH OVERRIDE; GO EXEC sp_configure 'optimize for ad hoc workloads', 1; GO RECONFIGURE WITH OVERRIDE;
A następnie zwolnij pamięć podręczną planu:
DBCC FREEPROCCACHE;
Teraz generujemy kilka prostych odmian zapytania, które poza tym jest identyczne. Te odmiany mogą potencjalnie reprezentować style kodowania dla dwóch różnych programistów – niewielkie różnice w białych znakach, wielkich/małych literach itp.
SELECT TOP (1) SalesOrderID, OrderDate, SubTotal FROM Sales.SalesOrderHeader WHERE SalesOrderID >= 75120 ORDER BY OrderDate DESC; GO -- change >= 75120 to > 75119 (same logic since it's an INT) GO SELECT TOP (1) SalesOrderID, OrderDate, SubTotal FROM Sales.SalesOrderHeader WHERE SalesOrderID > 75119 ORDER BY OrderDate DESC; GO -- change the query to all lower case GO select top (1) salesorderid, orderdate, subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO -- remove the parentheses around the argument for top GO select top 1 salesorderid, orderdate, subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO -- add a space after top 1 GO select top 1 salesorderid, orderdate, subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO -- remove the spaces between the commas GO select top 1 salesorderid,orderdate,subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO
Jeśli uruchomimy tę partię raz, a następnie sprawdzimy pamięć podręczną planu, zobaczymy, że mamy 6 kopii zasadniczo tego samego planu wykonania. Dzieje się tak, ponieważ tekst zapytania jest haszowany binarnie, co oznacza, że wielkość liter i białe znaki mają znaczenie i mogą sprawić, że identyczne zapytania będą wyglądały na unikalne dla SQL Server.
SELECT [text], size_in_bytes, usecounts, cacheobjtype FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t WHERE LOWER(t.[text]) LIKE '%ales.sales'+'orderheader%';
Wyniki:
tekst | rozmiar_w_bajtach | liczba użycia | cacheobjtype |
---|---|---|---|
wybierz 1 pierwszy identyfikator zamówienia sprzedaży, lub… | 272 | 1 | Skompilowany odcinek planu |
wybierz 1 pierwszy identyfikator zamówienia sprzedaży,… | 272 | 1 | Skompilowany odcinek planu |
wybierz 1 pierwszy identyfikator zamówienia sprzedaży, o… | 272 | 1 | Skompilowany odcinek planu |
wybierz pierwszy (1) identyfikator zamówienia sprzedaży,… | 272 | 1 | Skompilowany odcinek planu |
WYBIERZ TOP (1) ID zamówienia sprzedaży,… | 272 | 1 | Skompilowany odcinek planu |
WYBIERZ TOP (1) ID zamówienia sprzedaży,… | 272 | 1 | Skompilowany odcinek planu |
Wyniki po pierwszym wykonaniu „identycznych” zapytań
Nie jest to więc całkowicie marnotrawstwo, ponieważ ustawienie ad hoc pozwoliło SQL Serverowi na przechowywanie tylko małych kodów pośredniczących przy pierwszym wykonaniu. Jeśli jednak uruchomimy wsad ponownie (bez zwalniania pamięci podręcznej procedur), zobaczymy nieco bardziej niepokojący wynik:
tekst | rozmiar_w_bajtach | liczba użycia | cacheobjtype |
---|---|---|---|
wybierz 1 pierwszy identyfikator zamówienia sprzedaży, lub… | 49 152 | 1 | Skompilowany plan |
wybierz 1 pierwszy identyfikator zamówienia sprzedaży,… | 49 152 | 1 | Skompilowany plan |
wybierz 1 pierwszy identyfikator zamówienia sprzedaży, o… | 49 152 | 1 | Skompilowany plan |
wybierz pierwszy (1) identyfikator zamówienia sprzedaży,… | 49 152 | 1 | Skompilowany plan |
WYBIERZ TOP (1) ID zamówienia sprzedaży,… | 49 152 | 1 | Skompilowany plan |
WYBIERZ TOP (1) ID zamówienia sprzedaży,… | 49 152 | 1 | Skompilowany plan |
Wyniki po drugim wykonaniu „identycznych” zapytań
To samo dzieje się w przypadku zapytań parametrycznych, niezależnie od tego, czy parametryzacja jest prosta, czy wymuszona. To samo dzieje się, gdy ustawienie ad hoc nie jest włączone, z wyjątkiem tego, że dzieje się to wcześniej.
W rezultacie może to spowodować duże rozrost pamięci podręcznej planu, nawet w przypadku zapytań, które wyglądają identycznie — aż do dwóch zapytań, w których jeden programista wcina tabulator, a drugi 4 spacje. Nie muszę ci mówić, że próba wymuszenia tego rodzaju spójności w zespole może być nudna lub niemożliwa. Moim zdaniem daje to mocny ukłon w stronę modularyzacji, poddania się DRY i scentralizowania tego typu zapytań w jednej procedurze składowanej.
Zastrzeżenie
Oczywiście, jeśli umieścisz to zapytanie w procedurze składowanej, będziesz mieć tylko jedną jego kopię, więc całkowicie unikniesz możliwości posiadania wielu wersji zapytania z nieco innym tekstem zapytania. Teraz można również argumentować, że różni użytkownicy mogą tworzyć tę samą procedurę składowaną o różnych nazwach, a w każdej procedurze składowanej występuje niewielka różnica w tekście zapytania. Chociaż to możliwe, myślę, że stanowi to zupełnie inny problem. :-)