Jak zmierzyć lub znaleźć koszt utworzenia planu zapytań?

18

Mam typowy przypadek, w którym wąchanie parametrów powoduje, że „zły” plan wykonania ląduje w pamięci podręcznej planu, powodując, że kolejne wykonywanie mojej procedury składowanej jest bardzo wolne. Mogę „rozwiązać” ten problem za pomocą zmiennych lokalnych OPTIMIZE FOR ... UNKNOWNi OPTION(RECOMPILE). Jednak mogę również zagłębić się w kwerendę i spróbować ją zoptymalizować.

Próbuję ustalić, czy powinienem : mając ograniczony czas na naprawę problemów, chciałbym poznać koszt nie zrobienia tego. Moim zdaniem, jeśli się tylko trzymam OPTION(RECOMPILE), efektem netto jest to, że plan zapytania jest odtwarzany przy każdym uruchomieniu zapytania. Myślę, że muszę wiedzieć:

Jak dowiedzieć się, jaki jest koszt utworzenia planu zapytań?

Aby odpowiedzieć na moje pytanie, przejrzałem Google (np. Z tym zapytaniem ) i przejrzałem dokumentację kolumn dla dm_exec_query_statsDMV . Sprawdziłem także okno wyjściowe w SSMS pod kątem „Rzeczywistego planu zapytań”, aby znaleźć te informacje. Wreszcie, mam poszukiwanej DBA.SE . Żadna z nich nie doprowadziła do odpowiedzi.

Czy ktoś może mi powiedzieć? Czy jest możliwe znalezienie lub zmierzenie czasu potrzebnego na utworzenie planu?

Jeroen
źródło
5
Polecam pobrać kopię Optymalizatora zapytań SQL Server autorstwa Benjamina Nevareza . Jest wolne. Rozdział 5 „Proces optymalizacji” może pomóc w ustaleniu czasu kompilacji zapytania. Przynajmniej informuje o tym, przez co przechodzi optymalizator, aby utworzyć plan zapytań.
Mark Sinkinson

Odpowiedzi:

18

Jak dowiedzieć się, jaki jest koszt utworzenia planu zapytań?

Możesz spojrzeć na właściwości węzła głównego w planie zapytań, na przykład:

Wyciąg z właściwości korzenia
(zrzut ekranu z bezpłatnego Sentry One Plan Explorer )

Informacje te są również dostępne poprzez zapytanie o pamięć podręczną planu, na przykład za pomocą zapytania opartego na następujących relacjach:

WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT 
    CompileTime = c.value('(QueryPlan/@CompileTime)[1]', 'int'),
    CompileCPU = c.value('(QueryPlan/@CompileCPU)[1]', 'int'),
    CompileMemory = c.value('(QueryPlan/@CompileMemory)[1]', 'int'),
    ST.[text],
    QP.query_plan
FROM sys.dm_exec_cached_plans AS CP
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) AS QP
CROSS APPLY sys.dm_exec_sql_text(CP.plan_handle) AS ST
CROSS APPLY QP.query_plan.nodes('ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS N(c);

Fragment wyników

Aby uzyskać pełne omówienie dostępnych opcji obsługi tego rodzaju zapytań, zobacz niedawno zaktualizowany artykuł Erland Sommarskog .

Paul White przywraca Monikę
źródło
4

Zakładając, że „koszt” jest pod względem czasu (choć nie jestem pewien, co jeszcze może być pod względem ;-), to przynajmniej powinieneś być w stanie go zrozumieć, wykonując coś takiego:

DBCC FREEPROCCACHE WITH NO_INFOMSGS;

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases'; -- replace with your proc

SET STATISTICS TIME OFF;

Pierwszy element zgłaszany na karcie „Wiadomości” powinien być:

Czas analizy i kompilacji programu SQL Server:

Uruchomiłbym to co najmniej 10 razy i uśredniłem zarówno „CPU”, jak i „Elapsed” milisekundy.

Najlepiej byłoby uruchomić to w dziale Production, aby uzyskać prawdziwy szacunek czasu, ale rzadko ludzie mogą wyczyścić pamięć podręczną planu w Production. Na szczęście, począwszy od SQL Server 2008 stało się możliwe usunięcie konkretnego planu z pamięci podręcznej. W takim przypadku możesz wykonać następujące czynności:

DECLARE @SQL NVARCHAR(MAX) = '';
;WITH cte AS
(
  SELECT DISTINCT stat.plan_handle
  FROM sys.dm_exec_query_stats stat
  CROSS APPLY sys.dm_exec_text_query_plan(stat.plan_handle, 0, -1) qplan
  WHERE qplan.query_plan LIKE N'%sp[_]help%' -- replace "sp[_]help" with proc name
)
SELECT @SQL += N'DBCC FREEPROCCACHE ('
               + CONVERT(NVARCHAR(130), cte.plan_handle, 1)
               + N');'
               + NCHAR(13) + NCHAR(10)
FROM cte;
PRINT @SQL;
EXEC (@SQL);

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases' -- replace with your proc

SET STATISTICS TIME OFF;

Jednak w zależności od zmienności wartości przekazywanych dla parametru (ów) powodującego „zły” buforowany plan, istnieje inna metoda, aby uznać, że jest to środek pomiędzy : OPTION(RECOMPILE)a OPTION(OPTIMIZE FOR UNKNOWN)dynamicznym SQL. Tak, powiedziałem to. Mam na myśli nawet nie sparametryzowany dynamiczny SQL. Oto dlaczego.

Wyraźnie masz dane, które mają nierównomierny rozkład, przynajmniej pod względem jednej lub więcej wartości parametrów wejściowych. Wady wymienionych opcji to:

  • OPTION(RECOMPILE)wygeneruje plan dla każdego wykonania i nigdy nie będą mogli korzystać z dowolnego planu ponownego użycia, nawet jeśli wartości parametr przekazany ponownie są identyczne z wcześniejszym okresie (S). W przypadku proc, które są często wywoływane - raz na kilka sekund lub częściej - uratuje cię to od czasu do czasu okropnej sytuacji, ale wciąż pozostawia cię w niezbyt dobrej sytuacji.

  • OPTION(OPTIMIZE FOR (@Param = value)) wygeneruje plan na podstawie tej konkretnej wartości, co może pomóc w kilku przypadkach, ale nadal pozostawia Cię otwartym na bieżący problem.

  • OPTION(OPTIMIZE FOR UNKNOWN)wygeneruje plan na podstawie średniej dystrybucji, co pomoże niektórym zapytaniom, ale zaszkodzi innym. Powinno to być takie samo, jak opcja używania zmiennych lokalnych.

Jednak dynamiczny SQL, jeśli zostanie wykonany poprawnie , pozwoli, aby różne wartości były przekazywane, aby mieć własne oddzielne plany zapytań, które są idealne (cóż, o ile będą). Głównym kosztem jest to, że wraz ze wzrostem różnorodności przekazywanych wartości rośnie liczba planów wykonania w pamięci podręcznej i zajmują one pamięć. Niewielkie koszty to:

  • sprawdzanie poprawności parametrów ciągu, aby zapobiec wstrzyknięciom SQL

  • być może konieczne będzie skonfigurowanie Certyfikatu i Użytkownika opartego na Certyfikacie, aby zachować idealną abstrakcję bezpieczeństwa, ponieważ Dynamiczny SQL wymaga bezpośrednich uprawnień do tabeli.

Oto, jak poradziłem sobie z tą sytuacją, gdy miałem procy, które były wywoływane więcej niż raz na sekundę i trafiały na wiele tabel, każda z milionami wierszy. Próbowałem, OPTION(RECOMPILE)ale okazało się, że jest to zbyt szkodliwe dla procesu w 99% przypadków, w których nie wystąpił problem z wąchaniem parametru / błędnym planem buforowanym. I pamiętaj, że jeden z tych procesów zawierał około 15 zapytań i tylko 3 - 5 z nich zostało przekonwertowanych na Dynamic SQL, jak opisano tutaj; Dynamiczny SQL nie był używany, chyba że był konieczny dla konkretnego zapytania.

  1. Jeśli do procedury składowanej jest wiele parametrów wejściowych, dowiedz się, które z nich są używane z kolumnami, które mają bardzo zróżnicowane rozkłady danych (a zatem powodują ten problem), a które z nich są używane z kolumnami, które mają bardziej równomierne rozkłady (i nie powinny powodując ten problem).

  2. Zbuduj ciąg dynamicznego SQL przy użyciu parametrów parametrów wejściowych proc, które są powiązane z równomiernie rozmieszczonymi kolumnami. Ta parametryzacja pomaga zmniejszyć wynikowy wzrost planów wykonania w pamięci podręcznej związanej z tym zapytaniem.

  3. Dla pozostałych parametrów, które są powiązane z bardzo zróżnicowanymi dystrybucjami, należy je połączyć w Dynamiczny SQL jako wartości dosłowne. Ponieważ unikalne zapytanie jest określane przez wszelkie zmiany w tekście zapytania, posiadanie WHERE StatusID = 1jest innym zapytaniem, a zatem innym planem zapytań niż posiadanie WHERE StatusID = 2.

  4. Jeśli którykolwiek z parametrów wejściowych proc, które mają być konkatenowane w tekście zapytania, to ciągi, należy je zweryfikować, aby zabezpieczyć przed iniekcją SQL (choć jest to mniej prawdopodobne, jeśli przekazywane ciągi są generowane przez aplikacja, a nie użytkownik, ale nadal). Przynajmniej REPLACE(@Param, '''', '''''')upewnij się, że pojedyncze cudzysłowy stają się znakami pojedynczymi.

  5. W razie potrzeby utwórz certyfikat, który będzie używany do utworzenia użytkownika i podpisz procedurę składowaną, aby bezpośrednie uprawnienia do tabeli były przyznawane tylko nowemu użytkownikowi opartemu na certyfikacie, a nie użytkownikom [public]lub użytkownikom, którzy w przeciwnym razie nie powinni mieć takich uprawnień .

Przykładowy proces:

CREATE PROCEDURE MySchema.MyProc
(
  @Param1 INT,
  @Param2 DATETIME,
  @Param3 NVARCHAR(50)
)
AS
SET NOCOUNT ON;

DECLARE @SQL NVARCHAR(MAX);

SET @SQL = N'
     SELECT  tab.Field1, tab.Field2, ...
     FROM    MySchema.SomeTable tab
     WHERE   tab.Field3 = @P1
     AND     tab.Field8 >= CONVERT(DATETIME, ''' +
  CONVERT(NVARCHAR(50), @Param2, 121) +
  N''')
     AND     tab.Field2 LIKE N''' +
  REPLACE(@Param3, N'''', N'''''') +
  N'%'';';

EXEC sp_executesql
     @SQL,
     N'@P1 INT',
     @P1 = @Param1;
Solomon Rutzky
źródło
Dziękujemy za poświęcenie (dość czasu) odpowiedzi! Jestem trochę sceptyczny, jeśli chodzi o pierwszy kawałek, jak uzyskać czas kompilacji, biorąc pod uwagę, że jest to współczynnik 3 niższy niż wynik, który otrzymuję, stosując podejście @ PaulWhite . - Drugi bit dynamicznego SQL jest interesujący (choć wymagałby również czasu na wdrożenie; przynajmniej więcej niż tylko uderzenie OPTIONw moje zapytanie) i nie skrzywdziłby mnie zbytnio, ponieważ ten sproc jest dobrze wykorzystany w testach integracyjnych. - W każdym razie: dzięki za wgląd!
Jeroen