¿Por qué esta consulta se vuelve drásticamente más lenta cuando se envuelve en un TVF?

17

Tengo una consulta bastante compleja que se ejecuta en solo unos segundos por sí sola, pero cuando se ajusta a una función con valores de tabla, es mucho más lenta; En realidad no lo dejé terminar, pero se ejecuta por hasta diez minutos sin terminar. El único cambio es reemplazar dos variables de fecha (inicializadas con literales de fecha) con parámetros de fecha:

Corre en siete segundos

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

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

SELECT * FROM @Data

Corre por lo menos diez minutos

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

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

Anteriormente había escrito la función como un TVF de múltiples declaraciones con una cláusula RETURNS @Data TABLE (...), pero cambiar eso por la estructura en línea no ha hecho un cambio notable. El tiempo de ejecución largo del TVF es el SELECT * FROM Xtiempo real ; En realidad, crear el UDF solo lleva unos segundos.

Podría publicar la consulta en cuestión, pero es un poco larga (~ 165 líneas) y, en función del éxito del primer enfoque, sospecho que está sucediendo algo más. Ojeando los planes de ejecución, parecen ser idénticos.

He intentado dividir la consulta en secciones más pequeñas, sin cambios. Ninguna sección individual toma más de un par de segundos cuando se ejecuta sola, pero el TVF aún se cuelga.

Veo una pregunta muy similar, /programming/4190506/sql-server-2005-table-valued-function-weird-performance , pero no estoy seguro de que la solución se aplique. ¿Quizás alguien ha visto este problema y conoce una solución más general? ¡Gracias!

Aquí están las dm_exec_requests después de varios minutos de procesamiento:

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

Aquí está la 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 los oficios
fuente
¿Nos puede mostrar los planes de consulta de texto por favor? Y en la primera consulta, qué tipos son @StartDate + @EndDate
gbn
@gbn: Lo siento, el plan es demasiado largo, con aproximadamente 32K caracteres. ¿Hay algún subconjunto que sería más útil? Además, ¿preferiría el plan para la consulta independiente o el TVF?
Jon de todos los oficios
La ejecución del plan de ejecución en el formulario TVF de la consulta no devuelve información útil, por lo que supongo que está buscando el plan de consulta para la versión que no es TVF. ¿O hay alguna forma de llegar al plan de ejecución realmente utilizado por un TVF?
Jon de todos los oficios
Sin tareas de espera. No estoy familiarizado con dm_exec_requests, pero he agregado el resultado a partir de la marca de cinco minutos en la ejecución de TVF.
Jon of All Trades
@ Martin: Sí; la consulta independiente tuvo un tiempo de CPU de 7021 (2% de la versión parcial de TVF) y 154K de lecturas lógicas (0.5%). Recientemente dejé la versión de TVF para ejecutar, y terminó después de 27 minutos. Así que definitivamente está produciendo muchos más datos ... pero ¿cómo puedo lograr que use un plan mejor? Estudiaré el plan de buena ejecución en detalle y veré si algunas sugerencias ayudan.
Jon of All Trades

Respuestas:

3

Aislé el problema a una línea en la consulta. Teniendo en cuenta que la consulta tiene 160 líneas de largo, e incluyo las tablas relevantes de cualquier manera, si desactivo esta línea de la cláusula SELECT:

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

... el tiempo de ejecución se reduce de 63 minutos a cinco segundos (al incluir un CTE lo ha hecho un poco más rápido que la consulta original de siete segundos). Si se incluye ACS.AvgClickCosto GAAC.AvgAdCosthace que el tiempo de ejecución explote. Lo que lo hace especialmente extraño es que estos campos provienen de dos subconsultas que tienen, respectivamente, diez filas y tres. Cada uno de ellos se ejecuta en cero segundos cuando se ejecuta de forma independiente, y dado que los recuentos de filas son tan cortos, esperaría que el tiempo de unión sea trivial incluso con bucles anidados.

¿Alguna idea de por qué este cálculo aparentemente inofensivo arrojaría un TVF por completo, mientras se ejecuta muy rápidamente como una consulta independiente?

Jon de todos los oficios
fuente
He publicado la consulta, pero como puede ver, se basa en una docena de tablas, incluidas algunas vistas y otra TVF, así que me temo que no será útil. La parte que no entiendo es cómo envolver una consulta en un TVF puede multiplicar el tiempo de ejecución por 750. Solo sucede si incluyo GAAC.AvgAdCost(hoy; ayer ACS.AvgClickCosttambién fue un problema), por lo que esa subconsulta parece estar descartando el plan de ejecución .
Jon de todos los oficios
1
Supongo que tienes que mirar la cláusula de unión para las subconsultas. Si obtiene una relación de muchos a muchos entre cualquiera de las tablas, obtendrá 10 veces más registros para manejar.
En algún momento en nuestro proyecto (que tiene un montón de vistas anidadas y TVF en línea), nos encontramos reemplazar COALESCE()con ISNULL()vistas a la elaboración optimizador de consultas de mejores planes. Creo que tenía que ver con ISNULL()tener un tipo de salida más predecible que COALESCE(). ¿Vale la pena intentarlo? Sé que esto es vago, pero en nuestra experiencia limitada, influir en el optimizador de consultas hacia mejores planes parece un arte borroso, por lo que probar un montón de ideas vagas y locas por desesperación es la única forma en que hemos progresado.
2

Espero que esto tenga que ver con la detección de parámetros.

Algunos hablan sobre los problemas aquí (y puede buscar SO para detectar parámetros).

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

Hogan
fuente
No obtienes la detección de parámetros con TVF en línea: son solo macros que se expanden como vistas.
gbn
@gbn: Puede ser cierto que el TVF en sí se expande como una macro, pero (según tengo entendido) la consulta o sproc que finalmente ejecuta esa expansión está sujeta a planificación y posible parametrización. (Luchamos con esto en SQL Server 2005 hace un tiempo. La lucha fue especialmente difícil hasta que encontramos que SQL Server Management Studio usaba una configuración de sesión diferente ( ARITHABORT¿tal vez?) Que Reporting Services y / o jTDS, por lo que uno de ellos a veces aparecía un plan "malo" pero otros (irritantemente) lo harían bien "en la misma consulta".)
Me huele a olfatear ...
Hogan
Hmm, mucha lectura por hacer. Para lo que vale, no hay una gran diferencia en la cardinalidad para los valores parametrizados: la consulta incluye una tabla de fechas, con una sola fila por fecha, y varias otras tablas con muchas filas por fecha, pero aproximadamente el mismo número para cualquier fecha dada. Utilizo los mismos parámetros (21/05 al 23/05) en una ejecución de prueba inmediatamente después de (re) crear el UDF, por lo que, en todo caso, debería "cebarse" para esos valores.
Jon de todos los oficios
Una nota más: la asignación de los valores de los parámetros a las variables locales según lo descrito por Jetson en stackoverflow.com/questions/211355/… no tuvo un impacto material.
Jon of All Trades
1

Lamentablemente, el motor de optimización de consultas de SQL no puede ver las funciones internas.

Así que usaría el plan de ejecución del rápido para descubrir qué sugerencias aplicar en el TF. Enjuague y repita hasta que el plan de ejecución del TF se aproxime al más rápido.

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

cosecha316
fuente
2
El optimizador de consultas de SQL Server puede ver dentro de ITVF (funciones en línea con valores de tabla), pero no otras.
Nota: las funciones de tabla en línea con aplicación cruzada cuando están diseñadas correctamente pueden conducir a un gran impulso en el rendimiento. Por ejemplo, una expresión no retenible en una unión, como su fusión, podría incluirse en una instrucción de aplicación, evaluarse como un conjunto y luego unirse en la siguiente consulta sin que se convierta en RBAR. Experimenta un poco. La aplicación cruzada es difícil de dominar, ¡pero vale la pena!
SheldonH
0

¿Cuáles son las diferencias en estos valores por favor?

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

Se ha demostrado que estos (especialmente arithabort) afectan seriamente el rendimiento de la consulta de esta manera.

gbn
fuente
Esto se debe a que es una clave de caché de plan en lugar de algo sobre arithabortsí mismo, ¿no es así? Desde SQL Server 2005, pensé que esta configuración no tenía efecto mientras estuviera activa ansi_warnings. (En 2000, las vistas indexadas no se usarían si se configuran incorrectamente)
Martin Smith,
@ Martin: No tengo experiencia directa en esto, pero recordé haber leído cosas recientemente. Y encontrar algunas respuestas SO en él. Puede ayudar a OP, puede que no ... Editar: sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/… suspiro
gbn
He leído afirmaciones similares bastante inequívocas sobre SO. Nunca he visto nada que me permita reproducirlo por mí mismo ni ninguna explicación lógica de por qué la arithabortconfiguración debería tener una influencia tan dramática en el rendimiento, por lo que estoy un poco escéptico al respecto en este momento.
Martin Smith
ARITHABORT, ANSI_WARNINGS, ANSI_PADDING y ANSI_NULL son 1, el resto son NULL.
Jon of All Trades
Para su información, estoy trabajando completamente en SSMS, por lo que diferentes configuraciones en VS u otros clientes no están en cuestión.
Jon of All Trades