Dlaczego to zapytanie jest znacznie wolniejsze, gdy jest zapakowane w TVF?

17

Mam dość złożone zapytanie, które uruchamia się w ciągu kilku sekund samodzielnie, ale po zawarciu funkcji o wartości tabeli jest znacznie wolniejsze; Właściwie nie pozwoliłem, aby to się skończyło, ale działa przez dziesięć minut bez końca. Jedyną zmianą jest zastąpienie dwóch zmiennych daty (zainicjowanych literałami daty) parametrami daty:

Działa w ciągu siedmiu sekund

DECLARE @StartDate DATE = '2011-05-21'
DECLARE @EndDate   DATE = '2011-05-23'

DECLARE @Data TABLE (...)
INSERT INTO @Data(...) SELECT...

SELECT * FROM @Data

Działa przez co najmniej dziesięć minut

CREATE FUNCTION X (@StartDate DATE, @EndDate DATE)
  RETURNS TABLE AS RETURN
  SELECT ...

SELECT * FROM X ('2011-05-21', '2011-05-23')

Wcześniej napisałem tę funkcję jako wielowątkową TVF z klauzulą ​​RETURNS @Data TABLE (...), ale zamiana tej struktury wbudowanej nie spowodowała zauważalnej zmiany. Długi czas trwania TVF to SELECT * FROM Xczas rzeczywisty ; w rzeczywistości utworzenie UDF zajmuje tylko kilka sekund.

Mógłbym opublikować zapytanie, ale jest ono nieco długie (~ 165 linii) i na podstawie sukcesu pierwszego podejścia podejrzewam, że dzieje się coś innego. Przeglądając plany wykonania, wydają się być identyczne.

Próbowałem podzielić zapytanie na mniejsze sekcje, bez zmian. Żadna pojedyncza sekcja nie zajmuje więcej niż kilka sekund, gdy jest wykonywana sama, ale TVF nadal się zawiesza.

Widzę bardzo podobne pytanie, /programming/4190506/sql-server-2005-table-valued-function-weird-performance , ale nie jestem pewien, czy to rozwiązanie ma zastosowanie. Być może ktoś widział ten problem i zna bardziej ogólne rozwiązanie? Dzięki!

Oto dm_exec_requests po kilku minutach przetwarzania:

session_id              59
request_id              0
start_time              40688.46517
status                  running
command                 UPDATE
sql_handle              0x030015002D21AF39242A1101ED9E00000000000000000000
statement_start_offset  10962
statement_end_offset    16012
plan_handle             0x050015002D21AF3940C1E6B0040000000000000000000000
database_id                 21
user_id                 1
connection_id           314AE0E4-A1FB-4602-BF40-02D857BAD6CF
blocking_session_id         0
wait_type               NULL
wait_time                   0
last_wait_type          SOS_SCHEDULER_YIELD
wait_resource   
open_transaction_count  0
open_resultset_count    1
transaction_id              48030651
context_info            0x
percent_complete        0
estimated_completion_time   0
cpu_time                    344777
total_elapsed_time          348632
scheduler_id            7
task_address            0x000000045FC85048
reads                   1549
writes                  13
logical_reads           30331425
text_size               2147483647
language                us_english
date_format             mdy
date_first              7
quoted_identifier           1
arithabort              1
ansi_null_dflt_on       1
ansi_defaults           0
ansi_warnings           1
ansi_padding            1
ansi_nulls                  1
concat_null_yields_null 1
transaction_isolation_level 2
lock_timeout            -1
deadlock_priority           0
row_count                   105
prev_error              0
nest_level              1
granted_query_memory    170
executing_managed_code  0
group_id                2
query_hash              0xBE6A286546AF62FC
query_plan_hash         0xD07630B947043AF0

Oto pełne zapytanie:

CREATE FUNCTION Routine.MarketingDashboardECommerceBase (@StartDate DATE, @EndDate DATE)
RETURNS TABLE AS RETURN
    WITH RegionsByCode AS (SELECT CountryCode, MIN(Region) AS Region FROM Staging.Volusion.MarketingRegions GROUP BY CountryCode)
        SELECT
            D.Date, Div.Division, Region.Region, C.Category1, C.Category2, C.Category3,
            COALESCE(V.Visits,          0) AS Visits,
            COALESCE(Dem.Demos,         0) AS Demos,
            COALESCE(S.GrossStores,     0) AS GrossStores,
            COALESCE(S.PaidStores,      0) AS PaidStores,
            COALESCE(S.NetStores,       0) AS NetStores,
            COALESCE(S.StoresActiveNow, 0) AS StoresActiveNow
            -- This line causes the run time to climb from a few seconds to over an hour!
            --COALESCE(V.Visits,          0) * COALESCE(ACS.AvgClickCost, GAAC.AvgAdCost, 0.00) AS TotalAdCost
            -- This line alone does not inflate the run time
            --ACS.AvgClickCost
            -- This line is enough to increase the run time to at least a couple minutes
            --GAAC.AvgAdCost
        FROM
            --Dates AS D
            (SELECT SQLDate AS Date FROM Dates WHERE SQLDate BETWEEN @StartDate AND @EndDate) AS D
            CROSS JOIN (SELECT 'UK' AS Division UNION SELECT 'US' UNION SELECT 'IN' UNION SELECT 'Unknown') AS Div
            CROSS JOIN (SELECT Category1, Category2, Category3 FROM Routine.MarketingDashboardCampaignMap UNION SELECT 'Unknown', 'Unknown', 'Unknown') AS C
            CROSS JOIN (SELECT DISTINCT Region FROM Staging.Volusion.MarketingRegions) AS Region
            -- Visitors
            LEFT JOIN
                (
                SELECT
                    V.Date,
                    CASE    WHEN V.Country IN ('United Kingdom', 'Guernsey', 'Ireland', 'Jersey') THEN 'UK'
                        WHEN V.Country IN ('United States', 'Canada', 'Puerto Rico', 'U.S. Virgin Islands') THEN 'US'
                        ELSE 'IN' END AS Division,
                    COALESCE(MR.Region, 'Unknown') AS Region,
                    C.Category1, C.Category2, C.Category3,
                    SUM(V.Visits) AS Visits
                FROM
                             RawData.GoogleAnalytics.Visits        AS V
                    INNER JOIN Routine.MarketingDashboardCampaignMap AS C ON V.LandingPage = C.LandingPage AND V.Campaign = C.Campaign AND V.Medium = C.Medium AND V.Referrer = C.Referrer AND V.Source = C.Source
                    LEFT JOIN  Staging.Volusion.MarketingRegions     AS MR ON V.Country = MR.CountryName
                WHERE
                    V.Date BETWEEN @StartDate AND @EndDate
                GROUP BY
                    V.Date,
                    CASE    WHEN V.Country IN ('United Kingdom', 'Guernsey', 'Ireland', 'Jersey') THEN 'UK'
                        WHEN V.Country IN ('United States', 'Canada', 'Puerto Rico', 'U.S. Virgin Islands') THEN 'US'
                        ELSE 'IN' END,
                    COALESCE(MR.Region, 'Unknown'), C.Category1, C.Category2, C.Category3
                ) AS V ON D.Date = V.Date AND Div.Division = V.Division AND Region.Region = V.Region AND C.Category1 = V.Category1 AND C.Category2 = V.Category2 AND C.Category3 = V.Category3
            -- Demos
            LEFT JOIN
                (
                SELECT
                    OD.SQLDate,
                    G.Division,
                    COALESCE(MR.Region,   'Unknown') AS Region,
                    COALESCE(C.Category1, 'Unknown') AS Category1,
                    COALESCE(C.Category2, 'Unknown') AS Category2,
                    COALESCE(C.Category3, 'Unknown') AS Category3,
                    SUM(D.Demos) AS Demos
                FROM
                             Demos            AS D
                    INNER JOIN Orders           AS O  ON D."Order" = O."Order"
                    INNER JOIN Dates            AS OD ON O.OrderDate = OD.DateSerial
                    INNER JOIN MarketingSources AS MS ON D.Source = MS.Source
                    LEFT JOIN  RegionsByCode    AS MR ON MS.CountryCode = MR.CountryCode
                    LEFT JOIN
                        (
                        SELECT
                            G.TransactionID,
                            MIN (
                                CASE WHEN G.Country IN ('United Kingdom', 'Guernsey', 'Ireland', 'Jersey') THEN 'UK'
                                    WHEN G.Country IN ('United States', 'Canada', 'Puerto Rico', 'U.S. Virgin Islands') THEN 'US'
                                    ELSE 'IN' END
                                ) AS Division
                        FROM
                            RawData.GoogleAnalytics.Geography AS G
                        WHERE
                                TransactionDate BETWEEN @StartDate AND @EndDate
                            AND NOT EXISTS (SELECT * FROM RawData.GoogleAnalytics.Geography AS G2 WHERE G.TransactionID = G2.TransactionID AND G2.EffectiveDate > G.EffectiveDate)
                        GROUP BY
                            G.TransactionID
                        ) AS G  ON O.VolusionOrderID = G.TransactionID
                    LEFT JOIN  RawData.GoogleAnalytics.Referrers     AS R  ON O.VolusionOrderID = R.TransactionID AND NOT EXISTS (SELECT * FROM RawData.GoogleAnalytics.Referrers AS R2 WHERE R.TransactionID = R2.TransactionID AND R2.EffectiveDate > R.EffectiveDate)
                    LEFT JOIN  Routine.MarketingDashboardCampaignMap AS C  ON MS.LandingPage = C.LandingPage AND MS.Campaign = C.Campaign AND MS.Medium = C.Medium AND COALESCE(R.ReferralPath, '(not set)') = C.Referrer AND MS.SourceName = C.Source
                WHERE
                        O.IsDeleted = 'No'
                    AND OD.SQLDate BETWEEN @StartDate AND @EndDate
                GROUP BY
                    OD.SQLDate,
                    G.Division,
                    COALESCE(MR.Region,   'Unknown'),
                    COALESCE(C.Category1, 'Unknown'),
                    COALESCE(C.Category2, 'Unknown'),
                    COALESCE(C.Category3, 'Unknown')
                ) AS Dem ON D.Date = Dem.SQLDate AND Div.Division = Dem.Division AND Region.Region = Dem.Region AND C.Category1 = Dem.Category1 AND C.Category2 = Dem.Category2 AND C.Category3 = Dem.Category3
            -- Stores
            LEFT JOIN
                (
                SELECT
                    OD.SQLDate,
                    CASE WHEN O.VolusionCountryCode = 'GB' THEN 'UK'
                        WHEN A.CountryShortName IN ('U.S.', 'Canada', 'Puerto Rico', 'U.S. Virgin Islands') THEN 'US'
                        ELSE 'IN' END AS Division,
                    COALESCE(MR.Region,     'Unknown') AS Region,
                    COALESCE(CpM.Category1, 'Unknown') AS Category1,
                    COALESCE(CpM.Category2, 'Unknown') AS Category2,
                    COALESCE(CpM.Category3, 'Unknown') AS Category3,
                    SUM(S.Stores) AS GrossStores,
                    SUM(CASE WHEN O.DatePaid <> -1 THEN 1 ELSE 0 END) AS PaidStores,
                    SUM(CASE WHEN O.DatePaid <> -1 AND CD.WeekEnding <> OD.WeekEnding THEN 1 ELSE 0 END) AS NetStores,
                    SUM(CASE WHEN O.DatePaid <> -1 THEN SH.ActiveStores ELSE 0 END) AS StoresActiveNow
                FROM
                             Stores           AS S
                    INNER JOIN Orders           AS O   ON S."Order" = O."Order"
                    INNER JOIN Dates            AS OD  ON O.OrderDate = OD.DateSerial
                    INNER JOIN Dates            AS CD  ON O.CancellationDate = CD.DateSerial
                    INNER JOIN Customers        AS C   ON O.CustomerNow = C.Customer
                    INNER JOIN MarketingSources AS MS  ON C.Source = MS.Source
                    INNER JOIN StoreHistory     AS SH  ON S.MostRecentHistory = SH.History
                    INNER JOIN Addresses        AS A   ON C.Address = A.Address
                    LEFT JOIN  RegionsByCode    AS MR  ON MS.CountryCode = MR.CountryCode
                    LEFT JOIN  Routine.MarketingDashboardCampaignMap AS CpM ON CpM.LandingPage = 'N/A' AND MS.Campaign = CpM.Campaign AND MS.Medium = CpM.Medium AND CpM.Referrer = 'N/A' AND MS.SourceName = CpM.Source
                WHERE
                        O.IsDeleted = 'No'
                    AND OD.SQLDate BETWEEN @StartDate AND @EndDate
                GROUP BY
                    OD.SQLDate,
                    CASE WHEN O.VolusionCountryCode = 'GB' THEN 'UK'
                        WHEN A.CountryShortName IN ('U.S.', 'Canada', 'Puerto Rico', 'U.S. Virgin Islands') THEN 'US'
                        ELSE 'IN' END,
                    COALESCE(MR.Region,     'Unknown'),
                    COALESCE(CpM.Category1, 'Unknown'),
                    COALESCE(CpM.Category2, 'Unknown'),
                    COALESCE(CpM.Category3, 'Unknown')
                ) AS S ON D.Date = S.SQLDate AND Div.Division = S.Division AND Region.Region = S.Region AND C.Category1 = S.Category1 AND C.Category2 = S.Category2 AND C.Category3 = S.Category3
            -- Google Analytics spend
            LEFT JOIN
                (
                SELECT
                    AC.Date, C.Category1, C.Category2, C.Category3, SUM(AC.AdCost) / SUM(AC.Visits) AS AvgAdCost
                FROM
                    RawData.GoogleAnalytics.AdCosts AS AC
                    INNER JOIN
                        (
                        SELECT Campaign, Medium, Source, MIN(Category1) AS Category1, MIN(Category2) AS Category2, MIN(Category3) AS Category3
                        FROM Routine.MarketingDashboardCampaignMap
                        WHERE Category1 <> 'Affiliate'
                        GROUP BY Campaign, Medium, Source
                        ) AS C ON AC.Campaign = C.Campaign AND AC.Medium = C.Medium AND AC.Source = C.Source
                WHERE
                    AC.Date BETWEEN @StartDate AND @EndDate
                GROUP BY
                    AC.Date, C.Category1, C.Category2, C.Category3
                HAVING
                    SUM(AC.AdCost) > 0.00 AND SUM(AC.Visits) > 0
                ) AS GAAC ON D.Date = GAAC.Date AND C.Category1 = GAAC.Category1 AND C.Category2 = GAAC.Category2 AND C.Category3 = GAAC.Category3
            -- adCenter spend
            LEFT JOIN
                (
                SELECT Date, SUM(Spend) / SUM(Clicks) AS AvgClickCost
                FROM RawData.AdCenter.Spend
                WHERE Date BETWEEN @StartDate AND @EndDate
                GROUP BY Date
                HAVING SUM(Spend) > 0.00 AND SUM(Clicks) > 0
                ) AS ACS ON D.Date = ACS.Date AND C.Category1 = 'PPC' AND C.Category2 = 'adCenter' AND C.Category3 = 'N/A'
        WHERE
            V.Visits > 0 OR Dem.Demos > 0 OR S.GrossStores > 0
GO


SELECT * FROM Routine.MarketingDashboardECommerceBase('2011-05-21', '2011-05-23')
Jon of All Trades
źródło
Czy możesz nam pokazać plany zapytań tekstowych? I w pierwszym zapytaniu, jakie są typy @StartDate + @EndDate
gbn
@gbn: Niestety, plan jest za długi i ma około 32 000 znaków. Czy jest jakiś podzbiór, który byłby najbardziej użyteczny? Czy wolałbyś również plan samodzielnego zapytania lub TVF?
Jon of All Trades
Uruchomienie planu wykonania w formie zapytania TVF nie zwraca żadnych użytecznych informacji, więc zakładam, że szukasz planu zapytań dla wersji innej niż TVF. Czy jest jakiś sposób, aby dostać się do planu wykonania faktycznie używanego przez TVF?
Jon of All Trades
Brak zadań oczekujących. Nie jestem zaznajomiony z dm_exec_requests, ale dołączyłem wynik z pięciominutowym znakiem w wykonaniu TVF.
Jon of All Trades
@Martin: Tak; samodzielne zapytanie miało czas procesora 7021 (2% częściowej wersji TVF) i logiczne odczyty 154K (0,5%). Niedawno opuściłem wersję TVF, aby uruchomić i zakończyła się po 27 minutach. Więc zdecydowanie przegląda znacznie więcej danych ... ale jak mogę zmusić go do korzystania z lepszego planu? Dokładnie przestudiuję dobry plan wykonania i zobaczę, czy pomoże kilka wskazówek.
Jon of All Trades

Odpowiedzi:

3

Wyodrębniłem problem do jednej linii w zapytaniu. Pamiętając, że zapytanie ma długość 160 wierszy, i tak czy inaczej dołączam odpowiednie tabele, jeśli wyłączę ten wiersz z klauzuli SELECT:

COALESCE(V.Visits, 0) * COALESCE(ACS.AvgClickCost, GAAC.AvgAdCost, 0.00)

... czas pracy spada z 63 minut do pięciu sekund (wprowadzenie CTE sprawiło, że jest on nieco szybszy niż oryginalne siedmiosekundowe zapytanie). Łącznie albo ACS.AvgClickCostalbo GAAC.AvgAdCostpowoduje, że czas pracy do eksplodować. Szczególnie dziwne jest to, że pola te pochodzą z dwóch podkwerend, które mają odpowiednio dziesięć wierszy i trzy! Każdy z nich działa w zero sekund, gdy działa niezależnie, a liczba wierszy jest tak krótka, że ​​spodziewałbym się, że czas łączenia będzie trywialny, nawet przy użyciu zagnieżdżonych pętli.

Czy są jakieś przypuszczenia, dlaczego to pozornie nieszkodliwe obliczenie całkowicie wyrzuciłoby TVF, podczas gdy działa ono bardzo szybko jako samodzielne zapytanie?

Jon of All Trades
źródło
Wysłałem zapytanie, ale jak widać rysuje się na kilkunastu stołach, w tym niektórych widokach i jednym innym TVF, więc obawiam się, że to nie pomoże. Nie rozumiem tylko tego, w jaki sposób zawinięcie zapytania w TVF może zwielokrotnić czas wykonywania przez 750. Dzieje się tak tylko wtedy, gdy dołączę GAAC.AvgAdCost(dzisiaj; wczoraj ACS.AvgClickCostrównież był problem), więc podzapytanie wydaje się odrzucać plan wykonania .
Jon of All Trades
1
Myślę, że musisz spojrzeć na klauzulę łączenia dla podkwerend. Jeśli uzyskasz relację wiele do wielu między dowolnymi tabelami, otrzymasz 10 razy więcej rekordów do obsługi.
W pewnym momencie naszego projektu (który ma wiele zagnieżdżonych widoków i wbudowanych TVF), zastąpiliśmy COALESCE()go, ISNULL()aby pomóc optymalizatorowi zapytań przygotować lepsze plany. Myślę, że miało to związek z ISNULL()bardziej przewidywalnym rodzajem wyjścia niż COALESCE(). Warte spróbowania? Wiem, że jest to niejasne, ale z naszego ograniczonego doświadczenia wynika, że ​​wpływanie na optymalizator zapytań w kierunku lepszych planów wydaje się zamazaną sztuką, więc wypróbowanie kilku nieokreślonych szalonych pomysłów z desperacji jest jedynym sposobem, w jaki zrobiliśmy postęp.
2

Oczekuję, że ma to związek z wąchaniem parametrów.

Niektóre rozmowy na temat problemów są tutaj (i możesz przeszukiwać SO dla wąchania parametrów).

http://blogs.msdn.com/b/queryoptteam/archive/2006/03/31/565991.aspx

Hogan
źródło
Nie dostajesz wąchania parametrów w przypadku wbudowanych TVF: są to tylko makra, które rozwijają się jak widoki.
gbn
@gbn: Może prawdą jest, że sam TVF jest rozwijany jak makro, ale (jak rozumiem) zapytanie lub sproc, który ostatecznie wykonuje to rozszerzenie, podlega planowaniu i potencjalnej parametryzacji. (Walczyliśmy z tym w SQL Server 2005 jakiś czas temu. Walka była szczególnie trudna, dopóki nie znaleźliśmy SQL Server Management Studio przy użyciu innych ustawień sesji ( ARITHABORTmoże?) Niż Reporting Services i / lub jTDS, więc jeden z nich czasami wymyślił „zły” plan, ale inni (irytująco) zrobiliby to dobrze „na tym samym zapytaniu”.)
Pachnie mnie węsząc ....
Hogan
Hmm, dużo do zrobienia. Jeśli chodzi o to, co jest warte, nie ma dużej różnicy w liczności sparametryzowanych wartości: zapytanie zawiera tabelę Daty z jednym wierszem na datę i kilka innych tabel z wieloma wierszami na datę, ale mniej więcej taką samą liczbę dla dowolnej daty. Używam tych samych parametrów (od 05/21 do 05/23) w wykonywaniu testu natychmiast po (ponownym) utworzeniu UDF, więc jeśli już, to powinien być „przygotowany” dla tych wartości.
Jon of All Trade
Jeszcze jedna uwaga: przypisanie wartości parametrów do zmiennych lokalnych zgodnie z opisem Jetsona w stackoverflow.com/questions/211355/... nie miało istotnego wpływu.
Jon of All Trades
1

Niestety silnik optymalizacji zapytań SQL nie widzi funkcji wewnętrznych.

Wykorzystam więc plan wykonania z szybkiego, aby dowiedzieć się, jakie wskazówki zastosować w TF. Płucz i powtarzaj, aż plan wykonania TF zbliży się do planu szybszego.

http://sqlblog.com/blogs/tibor_karaszi/archive/2008/08/29/execution-plan-re-use-sp-executesql-and-tsql-variables.aspx

żniwa316
źródło
2
SQL Server Query Optimizer może zobaczyć wewnątrz ITVF (wbudowane funkcje wartościowane w tabeli), ale nie ma innych.
Uwaga: funkcje tabeli wbudowanej z krzyżem mają zastosowanie, gdy prawidłowo zaprojektowane mogą prowadzić do ogromnego wzrostu wydajności. Na przykład niewyrażalne wyrażenie na złączeniu, takie jak twoja koalescencja, może być owinięte w instrukcji apply, ocenione jako zbiór, a następnie połączone w następnym zapytaniu bez zmiany RBAR. Eksperymentuj trochę. Stosowanie krzyżowe jest trudne do opanowania, ale warto!
SheldonH
0

Jakie są różnice między tymi wartościami?

arithabort              1
ansi_null_dflt_on       1
ansi_defaults           0
ansi_warnings           1
ansi_padding            1
ansi_nulls              1

Wykazano, że te (szczególnie arytabort) poważnie wpływają na wydajność zapytań w ten sposób.

gbn
źródło
Wynika to z faktu, że jest to klucz pamięci podręcznej planu, a nie coś o arithabortsobie, prawda? Od czasu SQL Server 2005 myślałem, że to ustawienie nie ma wpływu, dopóki ansi_warningsjest włączone. (W 2000 r. Indeksowane widoki nie byłyby używane, gdyby zostały ustawione niepoprawnie)
Martin Smith
@Martin: Nie mam bezpośredniego doświadczenia, ale ostatnio przypomniałem sobie o czytaniu. I znalezienie na to kilku SO odpowiedzi. Może pomóc OP, może nie ... Edytuj: sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/... westchnienie
gbn
Przeczytałem podobne dość jednoznaczne twierdzenia dotyczące SO. Nigdy nie widziałem niczego, co pozwoliłoby mi to odtworzyć dla siebie, ani żadnego logicznego wyjaśnienia, dlaczego to arithabortustawienie powinno mieć tak dramatyczny wpływ na wydajność, więc jestem w tej chwili nieco sceptyczny.
Martin Smith
ARITHABORT, ANSI_WARNINGS, ANSI_PADDING i ANSI_NULL mają 1, reszta ma wartość NULL.
Jon of All Trades
Do Twojej wiadomości, pracuję całkowicie w SSMS, więc różne ustawienia w VS lub innych klientach nie są objęte problemem.
Jon of All Trades