Perché questa query diventa drasticamente più lenta se racchiusa in un TVF?


17

Ho una query abbastanza complessa che viene eseguita in pochi secondi da sola, ma quando racchiusa in una funzione valutata a livello di tabella, è molto più lenta; In realtà non l'ho lasciato finire, ma dura fino a dieci minuti senza interruzione. L'unica modifica è la sostituzione di due variabili di data (inizializzate con valori letterali di data) con parametri di data:

Funziona in sette secondi

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

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

SELECT * FROM @Data

Funziona per almeno dieci minuti

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

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

In precedenza avevo scritto la funzione come TVF multi-istruzione con una clausola RETURNS @Data TABLE (...), ma scambiando quella per la struttura inline non è stata apportata una modifica evidente. Il tempo di esecuzione a lungo termine di TVF è effettivoSELECT * FROM X tempo ; in realtà la creazione dell'UDF richiede solo pochi secondi.

Potrei pubblicare la domanda in questione, ma è un po 'lunga (~ 165 righe) e, in base al successo del primo approccio, sospetto che stia succedendo qualcos'altro. Scorrendo i piani di esecuzione, sembrano identici.

Ho provato a suddividere la query in sezioni più piccole, senza modifiche. Nessuna singola sezione richiede più di un paio di secondi quando eseguita da sola, ma il TVF si blocca ancora.

Vedo una domanda molto simile, /programming/4190506/sql-server-2005-table-valued-function-weird-performance , ma non sono sicuro che la soluzione si applichi. Forse qualcuno ha riscontrato questo problema e conosce una soluzione più generale? Grazie!

Ecco i dm_exec_requests dopo diversi minuti di elaborazione:

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

Ecco la query completa:

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

Puoi mostrarci i piani di query di testo per favore? E nella prima query, quali tipi sono @StartDate + @EndDate
gbn

@gbn: mi dispiace, il piano è troppo lungo, con circa 32K caratteri. C'è qualche sottoinsieme che sarebbe più utile? Inoltre, preferiresti il ​​piano per la query autonoma o TVF?
Jon of All Trades,

L'esecuzione del piano di esecuzione nel modulo TVF della query non restituisce informazioni utili, quindi suppongo che tu stia cercando il piano di query per la versione non TVF. O c'è un modo per arrivare al piano di esecuzione effettivamente utilizzato da un TVF?
Jon of All Trades,

Nessuna attività di attesa. Non ho familiarità con dm_exec_requests, ma ho aggiunto l'output a partire dal segno di cinque minuti nell'esecuzione del TVF.
Jon of All Trades,

@Martin: Sì; la query autonoma aveva un tempo della CPU di 7021 (2% della versione TVF parziale ) e 154K letture logiche (0,5%). Di recente ho lasciato funzionare la versione TVF, che è terminata dopo 27 minuti. Quindi sta decisamente sfornando molti più dati ... ma come posso farlo per usare un piano migliore? Studierò dettagliatamente il buon piano di esecuzione e vedrò se alcuni suggerimenti aiutano.
Jon of All Trades,

Risposte:


3

Ho isolato il problema su una riga della query. Tenendo presente che la query è lunga 160 righe e includo le tabelle pertinenti in entrambi i casi, se disabilito questa riga dalla clausola SELECT:

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

... il tempo di esecuzione scende da 63 minuti a cinque secondi (l'integrazione di un CTE lo ha reso leggermente più veloce della query originale di sette secondi). Compreso uno ACS.AvgClickCostoGAAC.AvgAdCost esplodere il tempo di esecuzione. Ciò che rende particolarmente strano è che questi campi provengano da due sottoquery che hanno rispettivamente dieci righe e tre! Ognuno di essi viene eseguito in zero secondi quando viene eseguito in modo indipendente e con i conteggi delle righe così brevi mi aspetterei che il tempo di join sia banale anche usando i loop nidificati.

Qualche ipotesi sul perché questo calcolo apparentemente innocuo eliminerebbe completamente un TVF, mentre viene eseguito molto rapidamente come query autonoma?


Ho pubblicato la query, ma come vedi si basa su una dozzina di tabelle, tra cui alcune viste e un'altra TVF, quindi temo che non sarà utile. La parte che non capisco è come il wrapping di una query in un TVF può moltiplicare il tempo di esecuzione per 750. Succede solo se includo GAAC.AvgAdCost(oggi; ieri ACS.AvgClickCostera anche un problema), quindi la subquery sembra buttare via il piano di esecuzione .
Jon of All Trades,

1
Immagino che tu debba guardare la clausola join per le subquery. Se ottieni molte o molte relazioni tra una qualsiasi delle tabelle, otterrai 10 volte più record da gestire.

A un certo punto del nostro progetto (che ha un sacco di punti di vista nidificati e TVFs in linea), ci siamo trovati di sostituire COALESCE()con ISNULL()la guida in piani il progetto di ottimizzazione delle query migliori. Penso che abbia avuto a che fare con ISNULL()un tipo di output più prevedibile di COALESCE(). Vale la pena provare? So che questo è vago, ma nella nostra esperienza limitata, influenzare Query Optimizer verso piani migliori sembra un'arte sfocata, quindi provare un mucchio di vaghe idee folli per disperazione è l'unico modo in cui abbiamo fatto progressi.

2

Mi aspetto che ciò abbia a che fare con lo sniffing dei parametri.

Alcuni parlano dei problemi qui (e puoi cercare SO per lo sniffing dei parametri).

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


Non si annusa il parametro con i TVF in linea: sono solo macro che si espandono come viste.
gbn,

@gbn: può essere vero che lo stesso TVF sia espanso come una macro, ma (come ho capito bene) la query o lo sproc che alla fine esegue tale espansione è soggetto alla pianificazione e alla potenziale parametrizzazione. (Abbiamo combattuto con questo in SQL Server 2005 un po 'di tempo fa. La lotta era particolarmente difficile fino a quando non abbiamo trovato SQL Server Management Studio utilizzando impostazioni di sessione diverse ( ARITHABORTforse?) Rispetto a Reporting Services e / o jTDS, quindi a volte uno di loro veniva fuori un piano "cattivo" ma altri farebbero (esasperatamente) bene "sulla stessa domanda".)

Odora di annusarmi per me ...
Hogan,

Hmm, molte letture da fare. Per quello che vale, non c'è una grande differenza nella cardinalità per i valori parametrizzati: la query include una tabella Date, con una singola riga per data e diverse altre tabelle con più righe per data, ma circa lo stesso numero per una determinata data. Uso gli stessi parametri (dal 21/05 al 23/05) in un'esecuzione di test immediatamente dopo (ri) la creazione dell'UDF, quindi se non altro dovrebbe essere "innescato" per quei valori.
Jon of All Trades,

Un'altra nota: l'assegnazione dei valori dei parametri alle variabili locali come descritto da Jetson in stackoverflow.com/questions/211355/… non ha avuto un impatto materiale.
Jon of All Trades,

1

Sfortunatamente il motore di ottimizzazione delle query di SQL non può vedere le funzioni interne.

Quindi userei il piano di esecuzione da quello veloce per capire quali suggerimenti applicare nel TF. Risciacquare e ripetere fino a quando il piano di esecuzione del TF si avvicina a quello più veloce.

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


2
Query Optimizer di SQL Server può vedere all'interno di ITVF (funzioni con valori di tabella incorporate), ma non altre.

Nota: le funzioni di tabella in linea con cross si applicano quando progettate correttamente possono portare a un enorme aumento delle prestazioni. Ad esempio, un'espressione non targetabile su un join come la tua coalesce, potrebbe essere racchiusa in un'istruzione apply, valutata come un set e quindi unita nella query successiva senza che diventi RBAR. Sperimenta un po '. L'applicazione incrociata è difficile da padroneggiare, ma ne vale la pena!
SheldonH

0

Quali sono le differenze tra questi valori, per favore?

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

Questi (specialmente arithabort) hanno dimostrato di influenzare seriamente le prestazioni della query in questo modo.


Questo perché si tratta di una chiave cache del piano piuttosto che di qualcosa su arithabortse stessa, no? Da SQL Server 2005 ho pensato che questa impostazione non avesse effetto finché ansi_warningsè attiva. (Nel 2000 le visualizzazioni indicizzate non verrebbero utilizzate se impostate in modo errato)
Martin Smith,

@Martin: non ho esperienza diretta di questo, ma mi sono ricordato di aver letto cose di recente. E trovare alcune risposte SO su di esso. Potrebbe aiutare OP, potrebbe non ... Modifica: sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/… sigh
gbn

Ho letto affermazioni simili inequivocabili su SO. Non ho mai visto nulla che mi permettesse di riprodurlo per me stesso o qualsiasi spiegazione logica del motivo per cui l' arithabortambiente avrebbe dovuto avere un'influenza così drammatica sulle prestazioni, quindi al momento sono un po 'scettico al riguardo.
Martin Smith,

ARITHABORT, ANSI_WARNINGS, ANSI_PADDING e ANSI_NULL sono 1, il resto è NULL.
Jon of All Trades,

Cordiali saluti, sto lavorando interamente in SSMS, quindi diverse impostazioni in VS o altri client non sono in discussione.
Jon of All Trades,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.