Почему этот запрос становится значительно медленнее, когда он обернут в 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')

Ранее я писал эту функцию как TVF с несколькими утверждениями с предложением 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.
Джон на все руки
@ Мартин: Да; автономный запрос имел процессорное время 7021 (2% от частичной версии TVF) и 154 тыс. логических чтений (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

Я ожидаю, что это связано с анализом параметров.

Некоторые разговоры о проблемах здесь (и вы можете найти SO для поиска параметров).

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

Hogan
источник
Вы не получаете сниффинг параметров со встроенными TVF: это просто макросы, которые расширяются как представления.
ГБН
@gbn: Это может быть правдой, что сам TVF расширяется как макрос, но (насколько я понимаю) запрос или фрагмент, который в конечном итоге выполняет это расширение, подлежит планированию и потенциальной параметризации. (Мы боролись с этим в SQL Server 2005 некоторое время назад. Борьба была особенно трудной, пока мы не нашли SQL Server Management Studio, использующей другие настройки сеанса ( ARITHABORTможет быть?), Чем Reporting Services и / или jTDS, поэтому один из них иногда приходил с «плохой» план, но другие (с бешенством) будут делать все
Это пахнет как нюхает меня ...
Хоган
Хм, много читать, чтобы сделать. Что бы это ни стоило, нет большой разницы в кардинальности для параметризованных значений: запрос включает в себя таблицу дат, с одной строкой на дату, и несколько других таблиц с большим количеством строк на дату, но примерно с тем же числом для любой данной даты. Я использую те же параметры (с 21.05 по 23.05) в тестовом выполнении сразу после (повторного) создания 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

harvest316
источник
2
Оптимизатор запросов SQL Server может видеть внутри ITVF (встроенные табличные функции), но не какие-либо другие.
Примечание: при правильном проектировании встроенные табличные функции с перекрестным применением могут привести к значительному увеличению производительности. Например, невыражаемое выражение в соединении, такое как ваше объединение, может быть заключено в инструкцию apply, оценено как набор, а затем объединено в следующем запросе, не становясь RBAR. Эксперимент немного. Крестовину сложно освоить, но оно того стоит!
SheldonH
0

Какие различия в этих значениях, пожалуйста?

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

Эти (особенно arithabort) таким образом серьезно влияют на производительность запросов.

ГБН
источник
Это потому, что это ключ кеша плана, а не что-то о arithabortсебе, не так ли? Начиная с SQL Server 2005, я думал, что этот параметр не влияет, пока включен ansi_warnings. (В 2000 индексированные представления не использовались бы, если бы были установлены неправильно)
Мартин Смит
@Martin: у меня нет прямого опыта в этом, но вспомнил, что недавно читал. И найти некоторые ТАК ответы на это. Это может помочь OP, это не может ... Edit: sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/... вздыхать
ГБН
Я читал подобные довольно однозначные претензии на SO. Я никогда не видел ничего такого, что позволило бы мне воспроизвести это для себя или какое-либо логическое объяснение того, почему arithabortнастройка должна иметь такое драматическое влияние на производительность, хотя я немного скептически отношусь к этому моменту.
Мартин Смит
ARITHABORT, ANSI_WARNINGS, ANSI_PADDING и ANSI_NULL равны 1, остальные равны NULL.
Джон на все руки
К вашему сведению, я полностью работаю в SSMS, поэтому другие настройки в VS или других клиентах не обсуждаются.
Джон на все руки