TVFにラップすると、このクエリが大幅に遅くなるのはなぜですか?


17

数秒で実行されるかなり複雑なクエリがありますが、テーブル値の関数にラップすると、はるかに遅くなります。実際には終了させて​​いませんが、終了せずに最大10分間実行されます。唯一の変更点は、2つの日付変数(日付リテラルで初期化された)を日付パラメーターに置き換えることです。

7秒で実行

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

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

SELECT * FROM @Data

少なくとも10分間実行

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として記述しましたが、インライン構造のそれを交換しても目立った変更はありませんでした。TVFの長い実行時間は実際のSELECT * FROM X時間です。実際にUDFを作成するには数秒かかります。

問題のクエリを投稿することはできますが、それは少し長く(〜165行)、最初のアプローチの成功に基づいて、何か他のことが起こっているのではないかと疑っています。実行計画をざっと見て、それらは同じように見えます。

クエリを変更せずに小さなセクションに分割してみました。1つのセクションを単独で実行した場合、数秒以上かかることはありませんが、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:申し訳ありませんが、プランは長すぎて約32K文字です。最も有用なサブセットはありますか?また、スタンドアロンクエリまたはTVFのプランを希望しますか?
にすべての取引のジョン

TVFフォームのクエリで実行プランを実行しても有用な情報は返されないため、非TVFバージョンのクエリプランを探していると思います。または、TVFが実際に使用する実行計画に到達する方法はありますか?
すべての取引のジョン

待機中のタスクはありません。私はdm_exec_requestsに精通していませんが、TVFの実行における5分マークの時点での出力を追加しました。
すべての取引のジョン

@マーティン:はい。スタンドアロンクエリのCPU時間は7021(部分的な TVFバージョンの2%)で、154Kの論理読み取り(0.5%)でした。私は最近TVFバージョンを残して実行しましたが、27分後に終了しました。だから、間違いなくはるかに多くのデータを駆け巡っています...しかし、どうすればより良い計画を使用することができますか?適切な実行計画を詳細に調査し、いくつかのヒントが役立つかどうかを確認します。
すべての取引のジョン

回答:


3

クエリの1行に問題を特定しました。クエリの長​​さは160行であり、SELECT句からこの行を無効にすると、どちらの方法でも関連するテーブルが含まれることに留意してください。

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

...実行時間が63分から5秒に短縮されます(CTEのインライン化により、元の7秒のクエリよりもわずかに高速になりました)。ACS.AvgClickCostまたはGAAC.AvgAdCostを含めると、ランタイムが爆発します。特に奇妙なのは、これらのフィールドがそれぞれ10行と3行の2つのサブクエリから来ていることです!それらはそれぞれ独立して実行されるとゼロ秒で実行され、行カウントが非常に短いため、ネストされたループを使用しても結合時間はささいであると予想されます。

どうやらこの一見無害な計算がTVFを完全にスローオフする一方で、スタンドアロンクエリとして非常に高速に実行される理由についての推測はありますか?


クエリを投稿しましたが、ご覧のとおり、いくつかのビューと1つのTVFを含む多数のテーブルに描画されているので、役に立たないのではないかと心配しています。私が理解していない部分は、TVFでクエリをラップすると実行時間が750倍になることです。これは、GAAC.AvgAdCost(今日;昨日ACS.AvgClickCostも問題でした)を含める場合にのみ発生するため、サブクエリは実行計画から外れているようです。
すべての取引のジョン

1
サブクエリのjoin句を確認する必要があると思います。テーブル間で多対多のリレーションを取得すると、処理するレコードが10倍増えます。

私たちのプロジェクト(ネストされたビューとインラインTVFが多数ある)のある時点で、クエリオプティマイザーがより良い計画を作成COALESCE()するのISNULL()を助けるために、私たちは自分自身に取って代わりました。私はそれがISNULL()より予測可能な出力タイプを持つことに関係していると思いますCOALESCE()。試すだけの価値があります?私はこれがあいまいであることを知っていますが、私たちの限られた経験では、より良いプランに向けてクエリオプティマイザーに影響を与えることは曖昧な芸術のようです。したがって、私たちが進歩した唯一の方法は、漠然としたクレイジーなアイデアを試すことです。

2

これはパラメータスニッフィングと関係があると思います。

問題に関するいくつかの話はここにあります(そして、パラメータスニッフィングのSOを検索できます)。

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


インラインTVFでパラメータースニッフィングを取得することはありません。これらは、ビューのように展開する単なるマクロです。
gbn

@gbn:TVF自体がマクロのように展開されるのは事実かもしれませんが、(私が理解しているように)最終的にその展開を実行するクエリまたはsprocは、計画および潜在的なパラメーター化の影響を受けます。(以前、SQL Server 2005でこれと戦っていましたARITHABORT。SQLServer Management Studio がReporting ServicesやjTDSとは異なるセッション設定(おそらく)を使用していることが判明するまで、戦いは特に困難でした。「悪い」計画ですが、他の人は(同じように)「同じクエリで」大丈夫です。)

それは....私に盗聴のようなにおいがする
ホーガン

うーん、やることがたくさんあります。価値のあるものとして、パラメーター化された値のカーディナリティに大きな違いはありません:クエリには、日付ごとに1行の日付テーブルと、日付ごとに多くの行があるが特定の日付とほぼ同じ数の他のテーブルが含まれます。UDFの作成(再)直後のテスト実行では、同じパラメーター(05/21から05/23)を使用しているため、これらの値に対して「準備」が必要です。
すべての取引のジョン

もう1つの注意点:Jetoverが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(インラインテーブル値関数)の内部見ることができますが、その他見ることができません。

注:正しく設計された場合、相互適用を伴うインラインテーブル関数は、パフォーマンスを大幅に向上させる可能性があります。たとえば、合体のような結合の非引数可能式は、適用ステートメントでラップされ、セットとして評価され、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:私はこれについて直接的な経験はありませんが、最近読んだものを思い出しました。そして、それに関するいくつかのSOの答えを見つけます。OPに役立つかもしれませんが、そうでないかもしれません...編集:sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/… ため息
gbn

私は、SOについて同様の非常に明白な主張を読みました。自分のためにそれを再現できるものやarithabort、パフォーマンスがパフォーマンスに劇的な影響を与える理由についての論理的な説明を見たことはありませんが、現時点では少し懐疑的です。
マーティンスミス

ARITHABORT、ANSI_WARNINGS、ANSI_PADDING、およびANSI_NULLは1で、残りはNULLです。
すべての取引のジョン

参考までに、私は完全にSSMSで作業しているので、VSや他のクライアントの異なる設定は問題になりません。
すべての取引のジョン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.