Mengapa kueri ini menjadi lebih lambat saat dibungkus TVF?

17

Saya memiliki permintaan yang cukup kompleks yang berjalan hanya dalam beberapa detik saja, tetapi ketika dibungkus dengan fungsi bernilai tabel, itu jauh lebih lambat; Saya sebenarnya tidak membiarkannya selesai, tapi itu berjalan hingga sepuluh menit tanpa berakhir. Satu-satunya perubahan adalah mengganti dua variabel tanggal (diinisialisasi dengan literal tanggal) dengan parameter tanggal:

Berjalan dalam Tujuh Detik

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

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

SELECT * FROM @Data

Berjalan setidaknya 10 menit

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

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

Saya sebelumnya telah menulis fungsi sebagai TVF multi-pernyataan dengan klausa RETURNS @Data TABLE (...), tetapi menukar bahwa untuk struktur inline belum membuat perubahan nyata. Waktu jangka panjang TVF adalah SELECT * FROM Xwaktu yang sebenarnya ; sebenarnya membuat UDF hanya membutuhkan beberapa detik.

Saya bisa memposting pertanyaan yang dimaksud, tetapi agak panjang (~ 165 baris) dan, berdasarkan keberhasilan pendekatan pertama, saya curiga ada sesuatu yang sedang terjadi. Melacak melalui rencana eksekusi, mereka tampaknya identik.

Saya sudah mencoba memecah kueri menjadi bagian yang lebih kecil, tanpa perubahan. Tidak ada satu bagian pun yang membutuhkan lebih dari beberapa detik ketika dieksekusi sendiri, tetapi TVF masih hang.

Saya melihat pertanyaan yang sangat mirip, /programming/4190506/sql-server-2005-table-valued-function-weird-performance , tapi saya tidak yakin bahwa solusinya berlaku. Mungkin seseorang telah melihat masalah ini dan tahu solusi yang lebih umum? Terima kasih!

Inilah dm_exec_requests setelah beberapa menit pemrosesan:

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

Inilah pertanyaan lengkapnya:

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 dari Semua Perdagangan
sumber
Bisakah Anda menunjukkan kepada kami rencana permintaan teks? Dan dalam permintaan pertama, tipe apa yang @StartDate + @EndDate
gbn
@ gbn: Maaf, rencananya terlalu panjang, sekitar 32 ribu karakter. Apakah ada beberapa himpunan bagian yang akan paling berguna? Juga, apakah Anda lebih suka rencana untuk permintaan yang berdiri sendiri atau TVF?
Jon dari Semua Perdagangan
Menjalankan rencana eksekusi pada bentuk TVF dari kueri tidak mengembalikan informasi yang berguna, jadi saya berasumsi Anda sedang mencari rencana kueri untuk versi non-TVF. Atau adakah cara untuk mencapai rencana eksekusi yang sebenarnya digunakan oleh TVF?
Jon dari Semua Perdagangan
Tidak ada tugas menunggu. Saya tidak terbiasa dengan dm_exec_requests, tapi saya sudah menambahkan output pada tanda lima menit dalam eksekusi TVF.
Jon of All Trades
@ Martin: Ya; kueri yang berdiri sendiri memiliki waktu CPU 7021 (2% dari versi TVF parsial ) dan 154K membaca logis (0,5%). Saya baru-baru meninggalkan versi TVF untuk menjalankan, dan selesai setelah 27 menit. Jadi ini pasti menghasilkan data yang jauh lebih banyak ... tetapi bagaimana saya bisa membuatnya menggunakan rencana yang lebih baik? Saya akan mempelajari rencana eksekusi yang baik secara terperinci dan melihat apakah beberapa petunjuk membantu.
Jon of All Trades

Jawaban:

3

Saya mengisolasi masalah ke satu baris dalam kueri. Ingatlah bahwa kueri memiliki panjang 160 baris, dan saya menyertakan tabel yang relevan, jika saya menonaktifkan baris ini dari klausa SELECT:

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

... run time turun dari 63 menit menjadi lima detik (sebaris CTE telah membuatnya sedikit lebih cepat daripada permintaan tujuh detik yang asli). Termasuk salah satu ACS.AvgClickCostatauGAAC.AvgAdCost menyebabkan run time meledak. Apa yang membuatnya sangat aneh adalah bahwa bidang ini berasal dari dua subquery yang masing-masing memiliki sepuluh baris dan tiga! Mereka masing-masing berjalan dalam nol detik ketika dijalankan secara independen, dan dengan jumlah baris yang begitu singkat, saya berharap waktu bergabungnya sepele bahkan menggunakan loop bersarang.

Adakah tebakan mengapa perhitungan yang tampaknya tidak berbahaya ini akan membuat TVF sepenuhnya, sementara itu berjalan sangat cepat sebagai permintaan yang berdiri sendiri?

Jon dari Semua Perdagangan
sumber
Saya telah memposting kueri, tetapi seperti yang Anda lihat itu menarik pada selusin tabel, termasuk beberapa pandangan dan satu TVF lainnya, jadi saya khawatir itu tidak akan membantu. Bagian yang saya tidak mengerti adalah bagaimana membungkus kueri di TVF dapat mengalikan run time dengan 750. Itu hanya terjadi jika saya memasukkan GAAC.AvgAdCost(hari ini; kemarin ACS.AvgClickCostjuga merupakan masalah), sehingga subquery tampaknya membuang rencana eksekusi .
Jon dari Semua Perdagangan
1
Saya kira Anda harus melihat klausa gabungan untuk subkueri. Jika Anda mendapatkan banyak ke banyak hubungan antara salah satu tabel, Anda akan mendapatkan catatan 10 kali lebih banyak untuk menangani.
Pada titik tertentu pada proyek kami (yang memiliki banyak tampilan bersarang dan TVF sebaris), kami menemukan diri kami menggantikan COALESCE()dengan ISNULL()untuk membantu pengoptimal rencana menyusun rencana yang lebih baik. Saya pikir itu ada hubungannya dengan ISNULL()memiliki tipe output yang lebih dapat diprediksi daripada COALESCE(). Layak dicoba? Saya tahu ini tidak jelas, tetapi dalam pengalaman kami yang terbatas, memengaruhi pengoptimal permintaan terhadap rencana yang lebih baik tampaknya seperti seni yang kabur, jadi mencoba sekelompok ide gila yang samar-samar karena putus asa adalah satu-satunya cara kami membuat kemajuan.
2

Saya berharap ini ada hubungannya dengan parameter sniffing.

Beberapa pembicaraan tentang masalah ada di sini (dan Anda dapat mencari SO untuk sniffing parameter.)

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

Hogan
sumber
Anda tidak mendapatkan sniffing parameter dengan TVF sebaris: mereka hanya makro yang berkembang seperti tampilan.
gbn
@ gbn: Mungkin benar bahwa TVF itu sendiri diperluas seperti makro, tetapi (seperti yang saya mengerti) permintaan atau sproc yang akhirnya mengeksekusi bahwa ekspansi tunduk pada perencanaan dan parameterisasi potensial. (Kami berjuang dengan ini di SQL Server 2005 beberapa waktu lalu. Pertarungan itu sangat sulit sampai kami menemukan SQL Server Management Studio menggunakan pengaturan sesi yang berbeda ( ARITHABORTmungkin?) Dari Layanan Pelaporan dan / atau jTDS, jadi salah satu dari mereka kadang-kadang akan datang dengan rencana "buruk" tetapi yang lain (dengan menyebalkan) akan baik-baik saja "pada permintaan yang sama".)
Baunya seperti mengendus bagiku ....
Hogan
Hmm, banyak membaca yang harus dilakukan. Untuk apa nilainya, tidak ada perbedaan besar dalam kardinalitas untuk nilai parameterisasi: kueri mencakup tabel Tanggal, dengan satu baris per tanggal, dan beberapa tabel lainnya dengan banyak baris per tanggal, tetapi tentang angka yang sama untuk tanggal tertentu. Saya menggunakan params yang sama (05/21 hingga 05/23) dalam pelaksanaan pengujian segera setelah (kembali) membuat UDF, jadi jika ada apa pun itu harus "prima" untuk nilai-nilai tersebut.
Jon of All Trades
Satu lagi catatan: menetapkan nilai parameter ke variabel lokal seperti yang dijelaskan oleh Jetson di stackoverflow.com/questions/211355/… tidak memiliki dampak material.
Jon of All Trades
1

Sayangnya mesin optimisasi query SQL tidak dapat melihat fungsi di dalamnya.

Jadi saya akan menggunakan rencana eksekusi dari yang cepat untuk mencari tahu petunjuk apa yang berlaku di TF. Bilas & ulangi sampai rencana eksekusi TF mendekati yang lebih cepat.

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

harvest316
sumber
2
SQL Server Query Optimizer dapat melihat di dalam ITVF (fungsi bernilai tabel inline), tetapi tidak yang lain.
Catatan: fungsi tabel inline dengan cross berlaku ketika dirancang dengan benar dapat menyebabkan peningkatan besar dalam kinerja. Misalnya, ekspresi nonsargable pada gabungan seperti gabungan Anda, dapat dibungkus dalam pernyataan terapan, dievaluasi sebagai set, dan kemudian bergabung melawan dalam kueri berikutnya tanpa menjadi RBAR. Bereksperimen sedikit. Salib berlaku sulit untuk dikuasai, tetapi sangat berharga!
SheldonH
0

Apa perbedaan dalam nilai-nilai ini?

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

Ini (terutama arithabort) telah terbukti secara serius mempengaruhi kinerja permintaan dengan cara ini.

gbn
sumber
Ini karena itu adalah kunci cache rencana daripada apa pun tentang arithabortdirinya sendiri, bukan? Sejak SQL Server 2005 saya pikir pengaturan ini tidak berpengaruh selama ansi_warningsaktif. (Pada 2000 tampilan yang diindeks tidak akan digunakan jika diatur secara tidak benar)
Martin Smith
@ Martin: Saya tidak punya pengalaman langsung tentang ini tetapi ingat membaca hal baru-baru ini. Dan menemukan beberapa jawaban SO di atasnya. Ini dapat membantu OP, mungkin tidak ... Edit: sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/… sigh
gbn
Saya telah membaca klaim yang hampir sama tentang SO. Saya belum pernah melihat sesuatu yang akan memungkinkan saya untuk mereproduksi untuk diri saya sendiri atau penjelasan logis mengapa arithabortpengaturan harus memiliki pengaruh dramatis pada kinerja meskipun jadi saya agak skeptis tentang hal itu saat ini.
Martin Smith
ARITHABORT, ANSI_WARNINGS, ANSI_PADDING, dan ANSI_NULL adalah 1, sisanya NULL.
Jon of All Trades
FYI, saya bekerja sepenuhnya dalam SSMS, jadi pengaturan berbeda di VS atau klien lain tidak dipermasalahkan.
Jon of All Trades