Por que essa consulta se torna drasticamente mais lenta quando envolvida em um TVF?

17

Eu tenho uma consulta bastante complexa que é executada em apenas alguns segundos sozinha, mas quando envolvida em uma função com valor de tabela, é muito mais lenta; Na verdade, eu não deixei terminar, mas é executado por até dez minutos sem terminar. A única alteração é substituir duas variáveis ​​de data (inicializadas com literais de data) por parâmetros de data:

É executado em sete segundos

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

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

SELECT * FROM @Data

É executado pelo menos dez minutos

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

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

Eu já havia escrito a função como um TVF de múltiplas instruções com uma cláusula RETURNS @Data TABLE (...), mas trocar isso pela estrutura em linha não fez uma alteração perceptível. O tempo de longo prazo do TVF é o SELECT * FROM Xtempo real ; na verdade, criar o UDF leva apenas alguns segundos.

Eu poderia postar a consulta em questão, mas é um pouco longa (~ 165 linhas) e, com base no sucesso da primeira abordagem, suspeito que algo está acontecendo. Percorrendo os planos de execução, eles parecem idênticos.

Tentei dividir a consulta em seções menores, sem alterações. Nenhuma seção isolada leva mais de alguns segundos quando executada sozinha, mas o TVF ainda trava.

Vejo uma pergunta muito semelhante, /programming/4190506/sql-server-2005-table-valued-function-weird-performance , mas não tenho certeza de que a solução se aplique. Talvez alguém tenha visto esse problema e saiba uma solução mais geral? Obrigado!

Aqui estão os dm_exec_requests após vários minutos de processamento:

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

Aqui está a consulta 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')
Jon de todos os comércios
fonte
Você pode nos mostrar os planos de consulta de texto, por favor? E na primeira consulta, que tipos são @StartDate + @EndDate
GBN
@gbn: Desculpe, o plano é muito longo, com cerca de 32 mil caracteres. Existe algum subconjunto que seria mais útil? Além disso, você prefere o plano para a consulta autônoma ou o TVF?
Jon of All Trades
A execução do plano de execução no formulário TVF da consulta não retorna nenhuma informação útil, portanto, suponho que você esteja procurando o plano de consulta para a versão não TVF. Ou existe alguma maneira de chegar ao plano de execução realmente usado por um TVF?
Jon of All Trades
Nenhuma tarefa em espera. Não estou familiarizado com dm_exec_requests, mas anexei a saída a partir da marca de cinco minutos na execução do TVF.
Jon of All Trades
@ Martin: Sim; a consulta independente teve tempo de CPU de 7021 (2% da versão TVF parcial ) e 154K leituras lógicas (0,5%). Recentemente, deixei a versão TVF em execução e ela terminou após 27 minutos. Definitivamente, está gerando muito mais dados ... mas como posso fazer com que use um plano melhor? Vou estudar o bom plano de execução em detalhes e ver se algumas dicas ajudam.
Jon of All Trades

Respostas:

3

Eu isolei o problema em uma linha na consulta. Lembre-se de que a consulta tem 160 linhas e estou incluindo as tabelas relevantes de qualquer maneira, se eu desativar essa linha da cláusula SELECT:

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

... o tempo de execução cai de 63 minutos para cinco segundos (incluir um CTE tornou-o um pouco mais rápido que a consulta original de sete segundos). A inclusão de ACS.AvgClickCostou GAAC.AvgAdCostfaz com que o tempo de execução exploda. O que o torna especialmente estranho é que esses campos vêm de duas subconsultas que possuem, respectivamente, dez linhas e três! Cada um deles é executado em zero segundos quando executado independentemente, e com a contagem de linhas sendo tão curta, eu esperaria que o tempo de junção fosse trivial mesmo usando loops aninhados.

Alguma sugestão sobre por que esse cálculo aparentemente inofensivo lançaria um TVF completamente, enquanto ele é executado muito rapidamente como uma consulta independente?

Jon de todos os comércios
fonte
Publiquei a consulta, mas como você pode ver, ela desenha em uma dúzia de tabelas, incluindo algumas visualizações e uma outra TVF, por isso temo que não seja útil. A parte que eu não entendo é como agrupar uma consulta em um TVF pode multiplicar o tempo de execução por 750. Isso só acontece se eu incluir GAAC.AvgAdCost(hoje; ontem ACS.AvgClickCosttambém foi um problema), de modo que a subconsulta parece estar descartando o plano de execução .
Jon de Todos os Negócios
1
Eu acho que você precisa examinar a cláusula join para as subconsultas. Se você tiver uma relação de muitos para muitos entre qualquer uma das tabelas, terá 10 vezes mais registros para manipular.
Em algum momento do nosso projeto (que tem muitas visualizações aninhadas e TVFs embutidos), nos encontramos substituindo COALESCE()por ISNULL()para ajudar o otimizador de consultas a elaborar melhores planos. Eu acho que tinha a ver com ISNULL()ter um tipo de saída mais previsível do que COALESCE(). Vale a pena tentar? Sei que isso é vago, mas em nossa experiência limitada, influenciar o otimizador de consultas em direção a melhores planos parece uma arte imprecisa; portanto, tentar um monte de idéias loucas vagas por desespero é a única maneira de progredir.
2

Espero que isso esteja relacionado ao parâmetro sniffing.

Algumas conversas sobre os problemas estão aqui (e você pode pesquisar no SO para detectar os parâmetros.)

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

Hogan
fonte
Você não consegue detectar os parâmetros com TVFs embutidos: são apenas macros que se expandem como visualizações.
gbn 24/05
@gbn: Pode ser verdade que o próprio TVF seja expandido como uma macro, mas (como eu o entendo) a consulta ou sproc que finalmente executa essa expansão está sujeito a planejamento e possível parametrização. (Lutamos contra isso no SQL Server 2005 há um tempo. A luta foi especialmente difícil até encontrarmos o SQL Server Management Studio usando configurações de sessão diferentes ( ARITHABORTtalvez?) Do que o Reporting Services e / ou o jTDS; portanto, um deles às vezes apresentava um plano "ruim", mas outros iriam (irritantemente) fazer o mesmo "na mesma consulta".)
Tem cheiro de sniffing para mim ....
Hogan
Hmm, muita leitura para fazer. Quanto vale a pena, não há grande diferença na cardinalidade para os valores parametrizados: a consulta inclui uma tabela Dates, com uma única linha por data, e várias outras tabelas com muitas linhas por data, mas aproximadamente o mesmo número para qualquer data. Eu uso os mesmos parâmetros (21/05 a 23/05) em uma execução de teste imediatamente após a (re) criação da UDF, portanto, se alguma coisa deve ser "preparada" para esses valores.
Jon of All Trades
Mais uma observação: atribuir os valores dos parâmetros a variáveis ​​locais, conforme descrito por Jetson em stackoverflow.com/questions/211355/… , não teve impacto material.
Jon of All Trades
1

Infelizmente, o mecanismo de otimização de consultas do SQL não pode ver as funções internas.

Então, eu usaria o plano de execução do rápido para descobrir quais dicas aplicar no TF. Enxágue e repita até que o plano de execução do TF se aproxime do mais rápido.

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

harvest316
fonte
2
O SQL Server Query Optimizer pode ver dentro do ITVF (funções com valor de tabela em linha), mas não em nenhum outro.
Nota: as funções de tabela em linha com aplicação cruzada quando projetadas corretamente podem levar a um enorme aumento no desempenho. Por exemplo, uma expressão não compartilhável em uma junção como sua coalescência pode ser agrupada em uma instrução de aplicação, avaliada como um conjunto e depois unida na próxima consulta sem que ela se torne RBAR. Experimente um pouco. Aplicação cruzada é difícil de dominar, mas vale a pena!
SheldonH 14/04
0

Quais são as diferenças nesses valores, por favor?

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

Demonstrou-se que esses (especialmente arithabort) afetam seriamente o desempenho da consulta dessa maneira.

gbn
fonte
Isso ocorre porque é uma chave de cache do plano e não algo sobre arithabortsi mesma, não é? Desde o SQL Server 2005, pensei que essa configuração não tivesse efeito enquanto estivesse ativa ansi_warnings. (Em 2000 exibições indexadas não seria usado se configurado incorretamente)
Martin Smith
@ Martin: Eu não tenho experiência direta com isso, mas lembrei-me de ler coisas recentemente. E encontrando algumas respostas para isso. Pode ajudar o OP, pode não ... Edit: sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/… sigh
gbn
Eu li afirmações bastante inequívocas semelhantes sobre o SO. Eu nunca vi nada que me permitisse reproduzi-lo para mim mesmo ou qualquer explicação lógica sobre o motivo pelo qual o arithabortcenário deveria ter uma influência tão dramática no desempenho, por isso estou um pouco cético sobre isso no momento.
Martin Smith
ARITHABORT, ANSI_WARNINGS, ANSI_PADDING e ANSI_NULL são 1, o restante é NULL.
Jon de Todos os Negócios
Para sua informação, estou trabalhando inteiramente no SSMS, portanto, configurações diferentes no VS ou em outros clientes não estão em questão.
Jon de Todos os Negócios