27 czerwca PASS Performance Virtual Chapter zorganizował Summer Performance Palooza 2013 – rodzaj zmniejszonych 24 godzin PASS, ale skupionych wyłącznie na tematach związanych z wydajnością. Prowadziłem sesję zatytułowaną „10 złych nawyków, które mogą zabić wydajność”, omawiając 10 następujących pojęć:
- WYBIERZ *
- Ślepe indeksy
- Brak przedrostka schematu
- Domyślne opcje kursora
- przedrostek sp_
- Zezwalanie na rozrost pamięci podręcznej
- Szerokie typy danych
- Domyślne ustawienia serwera SQL
- Nadużywanie funkcji
- „Działa na moim komputerze”
Być może pamiętasz niektóre z tych tematów z takich prezentacji, jak mój wykład „Złe nawyki i najlepsze praktyki” lub nasze cotygodniowe seminaria internetowe dotyczące dostrajania zapytań, które prowadzę z Kevinem Klinem od początku czerwca do tego tygodnia. (Nawiasem mówiąc, te 6 filmów będzie dostępnych na początku sierpnia w YouTube).
Na moją sesję wzięło udział 351 osób i otrzymałem świetne opinie. Chciałem poruszyć niektóre z nich.
Po pierwsze, kwestia konfiguracji:używałem zupełnie nowego mikrofonu i nie miałem pojęcia, że każde naciśnięcie klawisza będzie brzmiało jak grzmot. Rozwiązałem ten problem, poprawiając rozmieszczenie moich urządzeń peryferyjnych, ale chcę przeprosić wszystkich, których to dotyczy.
Następnie pliki do pobrania; pokład i próbki są publikowane na miejscu imprezy. Znajdują się na dole strony, ale możesz je również pobrać tutaj.
Na koniec poniżej znajduje się lista pytań, które zostały zadane podczas sesji i chciałem się upewnić, że odpowiedziałem na te, na które nie udzielono odpowiedzi podczas pytań i odpowiedzi na żywo. Przepraszam, że wcisnąłem to w niecały miesiąc , ale były dużo pytań i nie chciałem publikować ich w częściach.
P:Jeśli masz procedurę, która może mieć bardzo różne wartości wejściowe dla danych parametrów i w rezultacie w większości przypadków buforowany plan nie jest optymalny, najlepiej jest utworzyć procedurę WITH RECOMPILE i wziąć małą wydajność osiągana przy każdym uruchomieniu?
O: Będziesz musiał podejść do tego indywidualnie dla każdego przypadku, ponieważ będzie to naprawdę zależeć od wielu czynników (w tym złożoności planu). Należy również zauważyć, że można przeprowadzić ponowną kompilację na poziomie instrukcji w taki sposób, że tylko instrukcje, których dotyczy problem, muszą przyjąć trafienie, w przeciwieństwie do całego modułu. Paul White przypomniał mi, że ludzie często „naprawiają” wąchanie parametrów za pomocą RECOMPILE
, ale zbyt często oznacza to WITH RECOMPILE
w stylu 2000 zamiast znacznie lepszej OPTION (RECOMPILE)
, który nie tylko ogranicza się do instrukcji, ale także umożliwia osadzanie parametrów, które WITH RECOMPILE
nie. Tak więc, jeśli zamierzasz użyć RECOMPILE
aby udaremnić podsłuchiwanie parametrów, dodaj je do instrukcji, a nie do modułu.
O: Jak wyżej, będzie to zależeć od kosztów i złożoności planów i nie ma sposobu, aby powiedzieć „Tak, zawsze będzie duży hit wydajności”. Musisz także porównać to z alternatywą.
P:Jeśli istnieje indeks klastrowy w dniu insertdate, później, gdy pobieramy dane, używamy funkcji konwersji, jeśli używasz bezpośredniego porównania, data zapytania nie jest czytelna, w świecie rzeczywistym, jaki jest lepszy wybór?O: Nie jestem pewien, co oznacza „czytelny w prawdziwym świecie”. Jeśli masz na myśli, że chcesz uzyskać dane wyjściowe w określonym formacie, zwykle lepiej jest przekonwertować na ciąg po stronie klienta. C# i większość innych języków, których prawdopodobnie używasz w warstwie prezentacji, są w stanie sformatować dane wyjściowe daty/godziny z bazy danych w dowolnym formacie regionalnym.
P:Jak określić, ile razy używany jest plan w pamięci podręcznej – czy istnieje kolumna z tą wartością lub jakieś zapytania w Internecie, które dadzą tę wartość? Na koniec, czy takie liczby będą miały znaczenie tylko od ostatniego ponownego uruchomienia?O: Większość DMV jest ważna dopiero od ostatniego uruchomienia usługi, a nawet inne można opróżniać częściej (nawet na żądanie – zarówno nieumyślnie, jak i celowo). Pamięć podręczna planu jest oczywiście w ciągłym ruchu, a plany AFAIK, które wypadają z pamięci podręcznej, nie zachowują poprzedniej liczby, jeśli wrócą. Więc nawet gdy widzisz plan w pamięci podręcznej, nie jestem w 100% przekonany, że możesz uwierzyć w znalezioną liczbę zastosowań.
To powiedziawszy, prawdopodobnie szukasz sys.dm_exec_cached_plans.usecounts
możesz również znaleźć sys.dm_exec_procedure_stats.execution_count
aby pomóc w uzupełnieniu informacji o procedurach, w których poszczególne instrukcje w ramach procedur nie znajdują się w pamięci podręcznej.
O: Głównymi problemami związanymi z tym jest możliwość użycia określonej składni, takiej jak OUTER APPLY
lub zmienne z funkcją wycenianą w tabeli. Nie są mi znane przypadki, w których używanie niższej kompatybilności ma jakikolwiek bezpośredni wpływ na wydajność, ale kilka rzeczy zwykle zalecanych to przebudowa indeksów i aktualizacja statystyk (oraz aby dostawca jak najszybciej wspierał nowszy poziom kompatybilności). Widziałem, jak rozwiązało to nieoczekiwane pogorszenie wydajności w znacznej liczbie przypadków, ale słyszałem też opinie, że nie jest to konieczne, a może nawet nierozsądne.
O: Nie, przynajmniej pod względem wydajności, jeden wyjątek, w którym SELECT *
nie ma znaczenia, gdy jest używany wewnątrz EXISTS
klauzula. Ale dlaczego miałbyś używać *
tutaj? Wolę używać EXISTS (SELECT 1 ...
– optymalizator potraktuje je tak samo, ale w pewien sposób sam dokumentuje kod i zapewnia, że czytelnicy rozumieją, że podzapytanie nie zwraca żadnych danych (nawet jeśli przegapią duże EXISTS
na zewnątrz). Niektórzy używają NULL
, i nie mam pojęcia, dlaczego zacząłem używać 1, ale znalazłem NULL
trochę nieintuicyjne.
*Uwaga* musisz być ostrożny, jeśli spróbujesz użyć EXISTS (SELECT *
wewnątrz modułu, który jest powiązany ze schematem:
CREATE VIEW dbo.ThisWillNotWork WITH SCHEMABINDING AS SELECT BusinessEntityID FROM Person.Person AS p WHERE EXISTS (SELECT * FROM Sales.SalesOrderHeader AS h WHERE h.SalesPersonID = p.BusinessEntityID);
Pojawia się ten błąd:
Msg 1054, Poziom 15, Stan 6, Procedura ThisWillNotWork, Wiersz 6Składnia '*' nie jest dozwolona w obiektach powiązanych ze schematem.
Jednak zmieniając go na SELECT 1
działa dobrze. Może to kolejny argument za unikaniem SELECT *
nawet w tym scenariuszu.
O: Prawdopodobnie istnieją setki w różnych językach. Podobnie jak konwencje nazewnictwa, standardy kodowania są bardzo subiektywne. Tak naprawdę nie ma znaczenia, która konwencja, którą uznasz, będzie dla Ciebie najlepsza; jeśli lubisz tbl
przedrostki, zwariuj! Wolisz Pascala od bigEndianu. Chcesz poprzedzić nazwy kolumn typem danych, np. intCustomerID
, nie zamierzam cię zatrzymywać. Ważniejsze jest to, że definiujesz konwencję i stosujesz ją *konsekwentnie.*
To powiedziawszy, jeśli chcesz moich opinii, nie brakuje mi ich.
P:Czy XACT_ABORT może być używany w SQL Server 2008 i nowszych?
O: Nie znam żadnych planów wycofania XACT_ABORT
więc powinno nadal działać dobrze. Szczerze mówiąc, nie widzę tego zbyt często, ponieważ mamy TRY / CATCH
(i THROW
od SQL Server 2012).
O: Nie testowałem tego, ale w wielu przypadkach zastąpienie funkcji skalarnej wbudowaną funkcją z wartością tabelaryczną może mieć duży wpływ na wydajność. Problem, który znalazłem, polega na tym, że wprowadzenie tego przełącznika może wymagać znacznej ilości pracy w systemie, który został napisany przed APPLY
istniały lub nadal są zarządzane przez osoby, które nie przyjęły tego lepszego podejścia.
O: Przychodzą mi na myśl dwie rzeczy:(1) opóźnienie ma związek z czasem kompilacji lub (2) opóźnienie ma związek z ilością danych, które są ładowane w celu zaspokojenia zapytania i kiedy po raz pierwszy muszą pochodzić z dysku a nie pamięć. W przypadku (1) możesz wykonać zapytanie w Eksploratorze planów SQL Sentry, a pasek stanu pokaże czas kompilacji dla pierwszego i kolejnych wywołań (chociaż minuta wydaje się dość przesadna i mało prawdopodobna). Jeśli nie znajdziesz żadnej różnicy, może to być po prostu natura systemu:niewystarczająca pamięć do obsłużenia ilości danych, które próbujesz załadować za pomocą tego zapytania w połączeniu z innymi danymi, które już znajdowały się w puli buforów. Jeśli nie uważasz, że którykolwiek z tych problemów stanowi problem, sprawdź, czy te dwie różne egzekucje faktycznie dają różne plany — jeśli są jakieś różnice, opublikuj plany na adres Answers.sqlperformance.com, a z przyjemnością się przyjrzymy . W rzeczywistości przechwytywanie rzeczywistych planów dla obu wykonań przy użyciu Eksploratora planów w każdym przypadku może również poinformować o wszelkich różnicach we/wy i może prowadzić do tego, że SQL Server spędza swój czas przy pierwszym, wolniejszym uruchomieniu.
P:Otrzymuję podsłuchiwanie parametrów za pomocą sp_executesql, czy Optimize dla obciążeń ad hoc rozwiąże ten problem, ponieważ tylko fragment planu znajduje się w pamięci podręcznej?
O: Nie, nie sądzę, aby ustawienie Optymalizuj dla obciążeń ad hoc pomogło w tym scenariuszu, ponieważ podsłuchiwanie parametrów oznacza, że kolejne wykonania tego samego planu są używane dla różnych parametrów i ze znacznie różnymi zachowaniami wydajnościowymi. Optymalizacja pod kątem obciążeń ad hoc służy do minimalizowania drastycznego wpływu na pamięć podręczną planu, który może mieć miejsce w przypadku dużej liczby różnych instrukcji SQL. Więc jeśli nie mówisz o wpływie na pamięć podręczną planu wielu różnych instrukcji, które wysyłasz do sp_executesql
– co nie byłoby scharakteryzowane jako wąchanie parametrów – myślę, że eksperymentując z OPTION (RECOMPILE)
może mieć lepszy wynik lub, jeśli znasz wartości parametrów, które *dają* dobre wyniki w różnych kombinacjach parametrów, użyj OPTIMIZE FOR
. Ta odpowiedź Paula White'a może zapewnić znacznie lepszy wgląd.
O: Jasne, po prostu dołącz OPTION (RECOMPILE)
w dynamicznym tekście SQL:
DBCC FREEPROCCACHE; USE AdventureWorks2012; GO SET NOCOUNT ON; GO EXEC sp_executesql N'SELECT TOP (1) * INTO #x FROM Sales.SalesOrderHeader;'; GO EXEC sp_executesql N'SELECT TOP (1) * INTO #x FROM Sales.SalesOrderDetail OPTION (RECOMPILE);' GO SELECT t.[text], p.usecounts FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.[plan_handle]) AS t WHERE t.[text] LIKE N'%Sales.' + 'SalesOrder%';
Wyniki:1 wiersz pokazujący Sales.SalesOrderHeader
zapytanie.
Teraz, jeśli jakakolwiek instrukcja w partii NIE zawiera OPTION (RECOMPILE)
, plan może być nadal przechowywany w pamięci podręcznej, po prostu nie można go ponownie wykorzystać.
O: Cóż, BETWEEN
nie jest semantycznie równoważny z >= AND <
, ale raczej >= AND <=
, optymalizuje i działa dokładnie w ten sam sposób. W każdym razie celowo nie używam BETWEEN
w zapytaniach dotyczących zakresu dat – zawsze – ponieważ nie ma sposobu, aby był to zakres otwarty. Z BETWEEN
, oba końce są inkluzywne, co może być bardzo problematyczne w zależności od bazowego typu danych (teraz lub z powodu jakiejś przyszłej zmiany, o której możesz nie wiedzieć). Tytuł może wydawać się nieco surowy, ale szczegółowo omówię to w następującym poście na blogu:
Co łączy MIĘDZY diabłem i diabłem?
P:Co naprawdę robi „local fast_forward” w kursorze?
O: FAST_FORWARD
jest w rzeczywistości krótką formą READ_ONLY
i FORWARD_ONLY
. Oto, co robią:
LOCAL
sprawia, że zewnętrzne zakresy (domyślnie kursor toGLOBAL
chyba że zmieniłeś opcję na poziomie instancji).READ_ONLY
sprawia, że nie można bezpośrednio zaktualizować kursora, np. używającWHERE CURRENT OF
.FORWARD_ONLY
uniemożliwia przewijanie m.in. za pomocąFETCH PRIOR
lubFETCH ABSOLUTE
zamiastFETCH NEXT
.
Ustawienie tych opcji, jak zademonstrowałem (i pisałem o tym na blogu), może mieć znaczący wpływ na wydajność. Bardzo rzadko widzę kursory w środowisku produkcyjnym, które faktycznie muszą odbiegać od tego zestawu funkcji, ale zazwyczaj są one napisane tak, aby i tak akceptowały znacznie droższe wartości domyślne.
P:co jest bardziej wydajne, kursor czy pętla while?
O: WHILE
pętla będzie prawdopodobnie bardziej wydajna niż równoważny kursor z domyślnymi opcjami, ale podejrzewam, że nie zauważysz różnicy, jeśli użyjesz LOCAL FAST_FORWARD
. Ogólnie rzecz biorąc, WHILE
pętla *jest* kursorem, którego nie nazywa się kursorem, a w zeszłym roku rzuciłem wyzwanie kilku cenionym kolegom, aby udowodnili, że się mylę. Ich WHILE
pętle nie radziły sobie tak dobrze.
O: usp_
prefiks (lub dowolny prefiks inny niż sp_
, lub brak prefiksu w tej sprawie) *nie* ma takiego samego wpływu, jaki zademonstrowałem. Uważam jednak, że używanie prefiksu w procedurach składowanych jest mało wartościowe, ponieważ bardzo rzadko pojawia się wątpliwość, że kiedy znajdę kod, który mówi EXEC something
, że coś jest procedurą składowaną – więc nie ma tam wartości (w przeciwieństwie do, powiedzmy, przedrostków widoków w celu odróżnienia ich od tabel, ponieważ można ich używać zamiennie). Nadanie każdej procedurze tego samego prefiksu znacznie utrudnia również znalezienie poszukiwanego obiektu w, powiedzmy, Eksploratorze obiektów. Wyobraź sobie, że każde nazwisko w książce telefonicznej ma przedrostek LastName_
– w jaki sposób ci to pomaga?
O: Tak! Cóż, jeśli korzystasz z SQL Server 2008 lub nowszego. Gdy zidentyfikujesz dwa identyczne plany, nadal będą miały oddzielny plan_handle
wartości. Więc zidentyfikuj ten, którego *nie* chcesz zachować, skopiuj jego plan_handle
i umieść go w tym DBCC
polecenie:
DBCC FREEPROCCACHE(0x06.....);P:Czy użycie if else itp. w proc powoduje złe plany, czy jest on optymalizowany dla pierwszego uruchomienia i optymalizowany tylko dla tej ścieżki? Czy sekcje kodu w każdym IF muszą być podzielone na osobne procedury?
O: Ponieważ SQL Server może teraz przeprowadzać optymalizację na poziomie instrukcji, ma to obecnie mniej drastyczny efekt niż w przypadku starszych wersji, w których cała procedura musiała zostać ponownie skompilowana jako jedna jednostka.
P:Czasami zauważyłem, że pisanie dynamicznego sql może być lepsze, ponieważ eliminuje problem podsłuchiwania parametrów dla sp. Czy to prawda ? Czy w przypadku tego scenariusza należy dokonać kompromisów lub innych rozważań?O: Tak, dynamiczny SQL może często udaremniać podsłuchiwanie parametrów, szczególnie w przypadku, gdy masowe zapytanie „ulew kuchenny” zawiera wiele opcjonalnych parametrów. W powyższych pytaniach omówiłem kilka innych kwestii.
P:Gdybym miał kolumnę obliczeniową w mojej tabeli jako DATEPART(mojakolumna, rok) i w jej indeksie, czy serwer SQL używałby tego z SEEK?O: Powinno, ale oczywiście zależy to od zapytania. Indeks może nie być odpowiedni do pokrycia kolumn wyjściowych lub spełniania innych filtrów, a używany parametr może nie być wystarczająco selektywny, aby uzasadnić wyszukiwanie.
P:czy plan jest generowany dla KAŻDEGO zapytania? Czy plan jest generowany nawet dla trywialnych?O: O ile wiem, plan jest generowany dla każdego prawidłowego zapytania, nawet trywialnych planów, chyba że wystąpi błąd uniemożliwiający wygenerowanie planu (może się to zdarzyć w wielu scenariuszach, takich jak nieprawidłowe podpowiedzi). To, czy są przechowywane w pamięci podręcznej, czy nie (i jak długo pozostają w pamięci podręcznej), zależy od wielu innych czynników, z których niektóre omówiłem powyżej.
P:Czy wywołanie sp_executesql generuje (i ponownie wykorzystuje) plan z pamięci podręcznej?
O: Tak, jeśli wyślesz dokładnie ten sam tekst zapytania, nie ma znaczenia, czy wyślesz je bezpośrednio, czy wyślesz je przez sp_executesql
, SQL Server zapisze i ponownie użyje planu.
O: Nie rozumiem, dlaczego nie. Jedyną obawą, jaką mam, jest to, że przy natychmiastowej inicjalizacji plików programiści mogą nie zauważyć dużej liczby zdarzeń autowzrostu, co może odzwierciedlać słabe ustawienia autowzrostu, które mogą mieć bardzo różny wpływ na środowisko produkcyjne (zwłaszcza jeśli którykolwiek z tych serwerów *nie * mieć włączone IFI).
P:Czy w przypadku funkcji w klauzuli SELECT słuszne byłoby stwierdzenie, że lepiej jest zduplikować kod?
O: Osobiście powiedziałbym tak. Osiągnąłem dużo wydajności dzięki zastąpieniu funkcji skalarnych w SELECT
lista z wbudowanym odpowiednikiem, nawet w przypadkach, gdy muszę powtórzyć ten kod. Jak wspomniano powyżej, w niektórych przypadkach może się okazać, że zastąpienie tego wbudowaną funkcją z wartością tabeli może dać ci możliwość ponownego wykorzystania kodu bez nieprzyjemnych spadków wydajności.
O: Pochylenie danych może mieć czynnik i podejrzewam, że zależy to od rodzaju danych, które generujesz/symulujesz i jak daleko może być pochylenie. Jeśli masz, powiedzmy, kolumnę varchar(100), która w środowisku produkcyjnym ma zwykle długość 90 znaków, a generowanie danych generuje dane o średniej wartości 50 (co zakłada SQL Server), zobaczysz znacznie inny wpływ na liczbę stron i optymalizacji oraz prawdopodobnie niezbyt realistyczne testy.
Ale będę szczery:ten konkretny aspekt nie jest czymś, w co zainwestowałem dużo czasu, ponieważ zwykle potrafię zmusić mnie do zdobycia prawdziwych danych. :-)
P:Czy wszystkie funkcje tworzone są jednakowo podczas badania wydajności zapytań? Jeśli nie, czy istnieje lista znanych funkcji, których powinieneś unikać, gdy to możliwe?O: Nie, nie wszystkie funkcje są sobie równe pod względem wydajności. Istnieją trzy różne typy funkcji, które możemy tworzyć (na razie ignorując funkcje CLR):
- Wielozdaniowe funkcje skalarne
- Wieloinstancyjne funkcje z wartościami tabelarycznymi
- Inline funkcje z wartościami tabelarycznymi
Inline funkcje skalarne są wymienione w dokumentacji, ale są mitem i, od SQL Server Przynajmniej rok 2014 można również wymienić obok Sasquatcha i potwora z Loch Ness.
Ogólnie rzecz biorąc, gdybym mógł, umieściłbym to czcionką 80pt, wbudowane funkcje z wartościami tabelarycznymi są dobre, a innych należy unikać, jeśli to możliwe, ponieważ są znacznie trudniejsze do optymalizacji.
Funkcje mogą również mieć różne właściwości, które wpływają na ich wydajność, na przykład to, czy są deterministyczne i czy są powiązane ze schematem.
W przypadku wielu wzorców funkcji zdecydowanie należy wziąć pod uwagę wydajność, a także należy zdawać sobie sprawę z tego elementu Connect, który ma na celu ich rozwiązanie.
P:Czy możemy nadal uruchamiać podsumowania bez kursorów?O: Tak możemy; istnieje kilka metod innych niż kursor (jak opisano w moim poście na blogu, Najlepsze podejścia do obliczania sum – aktualizacja dla SQL Server 2012):
- Podzapytanie na liście SELECT
- Rekurencyjne CTE
- Samozłączenie
- "Dziwaczna aktualizacja"
- Tylko SQL Server 2012+:SUM() OVER() (przy użyciu domyślnej / RANGE)
- Tylko SQL Server 2012+:SUM() OVER() (przy użyciu ROWS)
Ostatnia opcja jest zdecydowanie najlepszym podejściem, jeśli korzystasz z SQL Server 2012; jeśli nie, istnieją ograniczenia dotyczące innych opcji bez kursora, które często sprawiają, że kursor jest bardziej atrakcyjnym wyborem. Na przykład dziwaczna metoda aktualizacji jest nieudokumentowana i nie ma gwarancji, że będzie działać w oczekiwanej kolejności; rekursywne CTE wymaga, aby nie było przerw w jakimkolwiek sekwencyjnym mechanizmie, którego używasz; a podzapytania i podejścia do samodzielnego łączenia po prostu się nie skalują.