Чому цей запит стає різко повільнішим, коли його загортають у TVF?


17

У мене досить складний запит, який працює за кілька секунд самостійно, але коли він перетворений на функцію, яка оцінюється за таблицею, це набагато повільніше; Я насправді не дав їй закінчитися, але це триває до десяти хвилин без кінця. Єдина зміна - це заміна двох змінних дати (ініціалізованих з літералами дати) параметрами дати:

Працює за сім секунд

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

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

SELECT * FROM @Data

Пробігає як мінімум десять хвилин

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

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

Раніше я писав функцію як телевізор із багатовиражними заявками з пунктом RETURNS @Data TABLE (...), але поміняючи, що для структури вбудованої структури не помітні зміни. Тривалий час роботи TVF - це фактичний SELECT * FROM Xчас; насправді створення UDF займає лише кілька секунд.

Я міг би розмістити запит, про який йде мова, але він трохи довгий (~ 165 рядків), і, виходячи з успіху першого підходу, я підозрюю, що відбувається ще щось. Проглядаючи плани виконання, вони здаються однаковими.

Я намагався порушити запит на менші розділи, без змін. Жоден розділ не займає більше пари секунд, коли він виконаний самостійно, але TVF все ще висить.

Я бачу дуже схоже запитання /programming/4190506/sql-server-2005-table-valued-function-weird-performance , але я не впевнений, що рішення застосовується. Можливо, хтось бачив цю проблему і знає більш загальне рішення? Спасибі!

Ось dm_exec_requests після декількох хвилин обробки:

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

Ось повний запит:

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')

Чи можете ви показати нам, будь ласка, плани текстового запиту? І в першому запиті, які типи є @StartDate + @EndDate
gbn

@gbn: Вибачте, план занадто довгий, близько 32 К символів. Чи є якась підмножина, яка була б найбільш корисною? Крім того, ви б віддали перевагу плану автономного запиту або TVF?
Джон з усіх торгів

Запуск плану виконання у формі запиту TVF не повертає корисної інформації, тому я припускаю, що ви шукаєте план запитів для не-TVF версії. Або є якийсь спосіб дістатися до плану виконання, який фактично використовує TVF?
Йон усіх торгів

Немає завдань очікування. Я не знайомий з dm_exec_requests, але додав висновок станом на п’ятихвилинну позначку у виконанні TVF.
Йон усіх торгів

@Martin: Так; окремий запит мав час процесора 7021 (2% від часткової версії TVF) та логічне зчитування 154K (0,5%). Нещодавно я залишив запуск версії TVF, і вона закінчилася через 27 хвилин. Тож, безумовно, йдеться про набагато більше даних ... але як я можу змусити його використовувати кращий план? Я детально вивчу хороший план виконання та побачу, чи допоможуть кілька підказок.
Йон усіх торгів

Відповіді:


3

Я виділив проблему в один рядок у запиті. Маючи на увазі, що запит завдовжки 160 рядків, і я включаю відповідні таблиці так чи інакше, якщо я відключую цей рядок із пункту SELECT:

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

... час запуску падає з 63 хвилин до п'яти секунд (вбудований CTE зробив це трохи швидше, ніж оригінальний семисекундний запит). У тому числі або ACS.AvgClickCostабо GAAC.AvgAdCostзмушує час виконання вибухати. Особливо дивним є те, що ці поля надходять із двох підзапитів, що мають відповідно десять рядків і три! Кожен з них виконується за нульові секунди при самостійному виконанні, і коли кількість рядків буде такою короткою, я б очікував, що час з'єднання буде тривіальним, навіть використовуючи вкладені петлі.

Будь-які здогадки, чому цей, здавалося б, нешкідливий розрахунок повністю скине TVF, при цьому він працює дуже швидко, як окремий запит?


Я розмістив запит, але, як ви бачите, він малює на десятках таблиць, включаючи деякі перегляди та ще один телевізор TVF, тож я побоююся, що він не буде корисним. Частина, яку я не розумію, - це те, як загортання запиту в TVF може помножити час виконання на 750. Це трапляється лише в тому випадку, якщо я включу GAAC.AvgAdCost(сьогодні; вчора ACS.AvgClickCostтакож була проблема), так що цей підзапит, здається, скидає план виконання .
Йон усіх торгів

1
Я думаю, вам доведеться переглянути підключення для підзапитів. Якщо ви отримаєте багато-багато відношення між будь-якою з таблиць, ви отримаєте в 10 разів більше записів для обробки.

В якому - то момент на нашому проекті (який має багато вкладених погляди і вбудований TVFs), ми виявили , замінивши COALESCE()з , ISNULL()щоб допомогти проекту оптимізатора запитів більш планів. Я думаю, що це стосувалося того, щоб ISNULL()мати більш передбачуваний тип виходу, ніж COALESCE(). Варто спробувати? Я знаю, що це розпливчасто, але, за нашого обмеженого досвіду, впливати на оптимізатор запитів щодо кращих планів здається нечітким мистецтвом, тому спроба кучки розпливчастих шалених ідей з відчаю - єдиний спосіб, коли ми просунулися.

2

Я думаю, це стосується нюху параметрів.

Деякі розмови про проблеми тут (і ви можете шукати ТА для обнюхування параметрів.)

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


Ви не отримуєте нюхання параметрів за допомогою вбудованих телевізійних каналів: вони просто макроси, які розширюються як перегляди.
gbn

@gbn: Можливо, правда, що сам TVF розширений як макрос, але (як я це розумію) запит або проросток, який в кінцевому рахунку виконує це розширення, підлягає плануванню та потенційній параметризації. (Ми з цим боролися з часом у SQL Server 2005. Бійка була особливо складною, поки ми не знайшли SQL Server Management Studio, використовуючи різні налаштування сеансу ( ARITHABORTможливо?), Ніж Reporting Services та / або jTDS, тому один із них іноді придумував би "поганий" план, але інші б (нахабно) зробили нормально "на той самий запит".)

Це пахне нюханням до мене ....
Хоган

Хм, багато читати. Для того, що це варто, немає великої різниці в кардинальності для параметризованих значень: запит включає таблицю Дати з одним рядком за датою та кілька інших таблиць з багатьма рядками на дату, але приблизно однакове число для будь-якої дати. Я використовую ті самі параметри (05/21 до 05/23) у тестовому виконанні одразу після (повторного) створення UDF, тому, якщо що-небудь, його слід "заправити" для цих значень.
Йон усіх торгів

Ще одна примітка: присвоєння значень параметрів локальним змінним, як описано Джетсоном в stackoverflow.com/questions/211355/… , не мало суттєвого впливу.
Джон з усіх торгів

1

На жаль, двигун оптимізації запитів SQL не може бачити внутрішні функції.

Тож я б скористався планом виконання від швидкого, щоб розібратися, які підказки застосувати в TF. Промийте та повторіть, поки план виконання TF не наблизиться до більш швидкого.

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


2
Оптимізатор запитів SQL Server може бачити всередині ITVF (функції вбудованої таблиці), але не будь-які інші.

Примітка: функції вбудованої таблиці з перехресним застосуванням, якщо правильно розроблені, можуть призвести до значного підвищення продуктивності. Наприклад, нерозбірливий вираз на з'єднанні, як у вашому coalesce, може бути загорнутий у оператор застосунку, оцінений як набір, а потім приєднатись до наступного запиту, не став RBAR. Експериментуйте трохи. Перехресне нанесення важко освоїти, але так варто!
SheldonH

0

Які відмінності у цих значеннях, будь ласка?

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

Показано, що ці (особливо арітаборти) серйозно впливають на ефективність запитів таким чином.


Це тому, що це кеш-план плану, а не щось про arithabortсебе, хоча це не так? Починаючи з SQL Server 2005, я вважав, що цей параметр не впливає, поки ansi_warningsвін увімкнено. (У 2000 році індексовані подання не будуть використовуватися, якщо їх встановлено неправильно)
Martin Smith

@Martin: Я не маю прямого досвіду цього, але згадав, що читав речі нещодавно. І знайти на цьому кілька відповідей. Це може допомогти ОП, а може не ... Редагувати: sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/… зітхнути
gbn

Я читав подібні досить однозначні претензії на SO. Я ніколи не бачив жодної речі, яка дозволила б мені відтворити це для себе, або будь-яке логічне пояснення того, чому arithabortпостановка повинна мати такий драматичний вплив на продуктивність, хоча я зараз трохи скептично ставлюсь до цього.
Мартін Сміт

ARITHABORT, ANSI_WARNINGS, ANSI_PADDING і ANSI_NULL - це 1, решта - NULL.
Йон усіх торгів

FYI, я повністю працюю в SSMS, тому різні налаштування в VS або інших клієнтах не підлягають сумніву.
Йон усіх торгів
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.