TVF로 래핑 할 때 왜이 쿼리가 크게 느려 집니까?


17

몇 초 안에 자체적으로 실행되는 상당히 복잡한 쿼리가 있지만 테이블 반환 함수로 래핑되면 속도가 훨씬 느려집니다. 실제로 끝내지 않았지만 끝내지 않고 최대 10 분 동안 실행됩니다. 유일한 변경 사항은 두 개의 날짜 변수 (날짜 리터럴로 초기화 됨)를 날짜 매개 변수로 바꾸는 것입니다.

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 줄) 첫 번째 접근 방식의 성공에 따라 다른 일이 진행되고 있다고 의심됩니다. 실행 계획을 살펴보면 동일한 것으로 보입니다.

쿼리를 변경하지 않고 더 작은 섹션으로 나누려고했습니다. 단독으로 실행될 때 단일 섹션이 몇 초 이상 걸리지 않지만 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

쿼리에서 한 줄로 문제를 격리했습니다. 쿼리의 길이는 160 줄이며 SELECT 절 에서이 줄을 비활성화하면 관련 테이블을 포함시킵니다.

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

... 실행 시간이 63 분에서 5 초로 감소했습니다 (CTE를 표시하면 원래 7 초 쿼리보다 약간 빠릅니다). 포함하거나 ACS.AvgClickCost또는 것은 GAAC.AvgAdCost폭발 실행 시간을 발생합니다. 특히 이상하게도이 필드는 각각 10 개의 행과 3 개의 서브 쿼리에서 온 것입니다! 그들은 독립적으로 실행될 때 각각 0 초 안에 실행되며 행 수가 너무 짧아서 중첩 루프를 사용하더라도 조인 시간이 사소한 것으로 기대합니다.

왜 이처럼 무해한 계산이 TVF를 완전히 중단시키는 지, 독립 실행 형 쿼리로 매우 빠르게 실행되는지에 대한 추측이 있습니까?


쿼리를 게시했지만 일부 뷰와 다른 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에서이 문제를 해결하기 위해 잠시 동안 싸웠습니다. ARITHABORTReporting Services 및 / 또는 jTDS와 다른 세션 설정 ( 아마?)을 사용하는 SQL Server Management Studio를 발견 할 때까지는 특히 어려움 이있었습니다. "나쁜"계획이지만 다른 사람들은 (같은

그것은 나에게 스니핑 냄새가 ....
Hogan

흠, 읽을 거리가 많다. 가치가있는 것은 매개 변수가 지정된 값의 카디널리티에 큰 차이가 없습니다. 쿼리에는 날짜 당 하나의 행이있는 날짜 테이블과 날짜 당 많은 행이 있지만 다른 날짜와 동일한 수의 다른 테이블이 포함됩니다. UDF 작성 (재) 후 즉시 테스트 실행에서 동일한 매개 변수 (05/21-05/23)를 사용하므로 해당 값에 대해 "프라이밍"해야합니다.
모든 거래의 존

참고 사항 : stackoverflow.com/questions/211355/… 에서 Jetson에 설명 된대로 매개 변수 값을 로컬 변수에 지정하는 것은 중대한 영향을 미치지 않습니다.
모든 거래의 존

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 내부 (인라인 테이블 반환 함수)를 볼 수 있지만 다른 것은 볼 수 없습니다.

참고 : 올바르게 설계된 경우 교차 적용 기능이있는 인라인 테이블 함수는 성능을 크게 향상시킬 수 있습니다. 예를 들어, 병합과 같은 결합에서 표현할 수없는 표현은 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 Smith

@ 마틴 : 나는 이것에 대한 직접적인 경험이 없지만 최근에 읽은 것을 회상했다. 그리고 그것에 대한 답변을 찾는 중입니다. 그것은 OP를 도울 수 있습니다, 그렇지 않을 수도 있습니다 ... 편집 : sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/… sigh
gbn

나는 SO에 대해 비슷한 명백한 주장을 읽었습니다. 나는 왜 그 arithabort설정이 성능에 극적인 영향을 미쳐야 하는지에 대한 나 자신이나 논리적 설명을 위해 그것을 재현 할 수있는 것을 보지 못했지만 지금은 그것에 대해 회의적입니다.
마틴 스미스

ARITHABORT, ANSI_WARNINGS, ANSI_PADDING 및 ANSI_NULL은 1이고 나머지는 NULL입니다.
모든 거래의 존

참고로, 나는 SSMS에서 전적으로 일하고 있으므로 VS 또는 다른 클라이언트의 다른 설정에는 문제가 없습니다.
모든 거래의 존
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.