Pourquoi cette requête devient-elle considérablement plus lente lorsqu'elle est enveloppée dans un TVF?

17

J'ai une requête assez complexe qui s'exécute en quelques secondes seule, mais lorsqu'elle est enveloppée dans une fonction table, elle est beaucoup plus lente; Je ne l'ai pas laissé terminer, mais il dure jusqu'à dix minutes sans se terminer. La seule modification consiste à remplacer deux variables de date (initialisées avec des littéraux de date) par des paramètres de date:

Fonctionne en sept secondes

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

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

SELECT * FROM @Data

Fonctionne pendant au moins dix minutes

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

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

J'avais précédemment écrit la fonction en tant que TVF à instructions multiples avec une clause RETURNS @Data TABLE (...), mais le remplacement de la structure en ligne n'a pas apporté de changement notable. Le temps de fonctionnement à long terme du TVF est le SELECT * FROM Xtemps réel ; la création de l'UDF ne prend que quelques secondes.

Je pourrais poster la requête en question, mais elle est un peu longue (~ 165 lignes) et, sur la base du succès de la première approche, je soupçonne que quelque chose d'autre se passe. Parcourant les plans d'exécution, ils semblent identiques.

J'ai essayé de diviser la requête en sections plus petites, sans changement. Aucune section ne prend plus de quelques secondes lorsqu'elle est exécutée seule, mais le TVF se bloque toujours.

Je vois une question très similaire, /programming/4190506/sql-server-2005-table-valued-function-weird-performance , mais je ne suis pas sûr que la solution s'applique. Peut-être que quelqu'un a vu ce problème et connaît une solution plus générale? Merci!

Voici les dm_exec_requests après plusieurs minutes de traitement:

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

Voici la requête complète:

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 tous les métiers
la source
Pouvez-vous nous montrer les plans de requête de texte s'il vous plaît? Et dans la première requête, quels types sont @StartDate + @EndDate
GBN
@gbn: Désolé, le plan est trop long, avec environ 32 000 caractères. Y a-t-il un sous-ensemble qui serait le plus utile? De plus, préférez-vous le plan pour la requête autonome ou le TVF?
Jon of All Trades
L'exécution du plan d'exécution sur le formulaire TVF de la requête ne renvoie aucune information utile, donc je suppose que vous recherchez le plan de requête pour la version non TVF. Ou existe-t-il un moyen d'obtenir le plan d'exécution réellement utilisé par un TVF?
Jon de tous les métiers
Aucune tâche en attente. Je ne connais pas dm_exec_requests, mais j'ai ajouté la sortie à partir de la marque de cinq minutes dans l'exécution du TVF.
Jon of All Trades,
@Martin: Oui; la requête autonome avait un temps CPU de 7021 (2% de la version TVF partielle ) et 154K lectures logiques (0,5%). J'ai récemment laissé la version TVF s'exécuter et elle s'est terminée après 27 minutes. Donc, il s'agit certainement de beaucoup plus de données ... mais comment puis-je l'amener à utiliser un meilleur plan? Je vais étudier en détail le bon plan d'exécution et voir si quelques astuces peuvent aider.
Jon of All Trades,

Réponses:

3

J'ai isolé le problème sur une ligne de la requête. En gardant à l'esprit que la requête fait 160 lignes et j'inclus les tables pertinentes de toute façon, si je désactive cette ligne à partir de la clause SELECT:

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

... le temps d'exécution passe de 63 minutes à cinq secondes (l'inclusion d'un CTE l'a rendu légèrement plus rapide que la requête originale de sept secondes). Y compris soit ACS.AvgClickCostouGAAC.AvgAdCost fait exploser le temps d'exécution. Ce qui le rend particulièrement étrange, c'est que ces champs proviennent de deux sous-requêtes qui ont respectivement dix lignes et trois! Ils s'exécutent chacun en zéro seconde lorsqu'ils sont exécutés indépendamment, et avec le nombre de lignes étant si court, je m'attends à ce que le temps de jointure soit trivial même en utilisant des boucles imbriquées.

Vous pensez que ce calcul apparemment inoffensif détruirait complètement un TVF, alors qu'il s'exécute très rapidement en tant que requête autonome?

Jon de tous les métiers
la source
J'ai posté la requête, mais comme vous pouvez le voir, elle s'appuie sur une douzaine de tableaux, y compris certaines vues et un autre TVF, donc je crains que cela ne soit pas utile. La partie que je ne comprends pas est de savoir comment envelopper une requête dans un TVF peut multiplier le temps d'exécution par 750. Cela ne se produit que si j'inclus GAAC.AvgAdCost(aujourd'hui; hier ACS.AvgClickCostétait également un problème), de sorte que la sous-requête semble rejeter le plan d'exécution .
Jon de tous les métiers
1
Je suppose que vous devez regarder la clause join pour les sous-requêtes. Si vous obtenez une relation plusieurs à plusieurs entre l'une des tables, vous obtiendrez 10 fois plus d'enregistrements à gérer.
À un moment donné de notre projet (qui a beaucoup de vues imbriquées et de TVF en ligne), nous nous sommes retrouvés à remplacer COALESCE()par ISNULL()pour aider l'optimiseur de requêtes à élaborer de meilleurs plans. Je pense que cela avait à voir avec ISNULL()un type de sortie plus prévisible queCOALESCE() . Ça vaut le coup d'essayer? Je sais que c'est vague, mais dans notre expérience limitée, influencer l'optimiseur de requêtes vers de meilleurs plans semble être un art flou, donc essayer un tas d'idées folles vagues par désespoir est la seule façon dont nous avons progressé.
2

Je pense que cela a à voir avec le reniflage des paramètres.

Certains parlent des problèmes sont ici (et vous pouvez rechercher SO pour le reniflage de paramètres.)

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

Hogan
la source
Vous n'obtenez pas de reniflement de paramètres avec les TVF en ligne: ce ne sont que des macros qui se développent comme des vues.
gbn
@gbn: Il est peut-être vrai que le TVF lui-même est développé comme une macro, mais (si je comprends bien) la requête ou le sproc qui exécute finalement cette expansion est soumis à la planification et au paramétrage potentiel. (Nous nous sommes battus avec cela dans SQL Server 2005 il y a quelque temps. Le combat était particulièrement difficile jusqu'à ce que nous trouvions SQL Server Management Studio utilisant des paramètres de session différents ( ARITHABORTpeut-être?) Que Reporting Services et / ou jTDS, de sorte que l'un d'entre eux proposait parfois un "mauvais" plan mais d'autres feraient (exaspérément) bien "sur la même requête".)
Ça sent le reniflement pour moi ....
Hogan
Hmm, beaucoup de lecture à faire. Pour ce que ça vaut, il n'y a pas de grande différence de cardinalité pour les valeurs paramétrées: la requête comprend une table Dates, avec une seule ligne par date, et plusieurs autres tables avec plusieurs lignes par date, mais à peu près le même nombre pour une date donnée. J'utilise les mêmes paramètres (21/05 au 23/05) dans une exécution de test immédiatement après la (re) création de l'UDF, donc si quelque chose il doit être "amorcé" pour ces valeurs.
Jon de tous les métiers,
Encore une remarque: l'attribution des valeurs des paramètres aux variables locales comme décrit par Jetson dans stackoverflow.com/questions/211355/… n'a pas eu d'impact significatif.
Jon of All Trades
1

Malheureusement, le moteur d'optimisation des requêtes de SQL ne peut pas voir les fonctions internes.

J'utiliserais donc le plan d'exécution du rapide pour comprendre quels conseils appliquer dans le TF. Rincer et répéter jusqu'à ce que le plan d'exécution du TF se rapproche du plus rapide.

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

récolte316
la source
2
L'optimiseur de requêtes SQL Server peut voir à l'intérieur d'ITVF (fonctions de valeur de table en ligne), mais pas les autres.
Remarque: les fonctions de table en ligne avec application croisée lorsqu'elles sont correctement conçues peuvent entraîner une énorme augmentation des performances. Par exemple, une expression non négociable sur une jointure comme votre fusion, peut être encapsulée dans une instruction apply, évaluée comme un ensemble, puis jointe dans la requête suivante sans qu'elle devienne RBAR. Expérimentez un peu. La candidature croisée est difficile à maîtriser, mais cela en vaut la peine!
SheldonH
0

Quelles sont les différences dans ces valeurs s'il vous plaît?

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

Il a été démontré que ceux-ci (en particulier arithabort) affectent sérieusement les performances des requêtes de cette manière.

gbn
la source
C'est parce qu'il s'agit d'une clé de cache de plan plutôt que de quelque chose sur arithabortlui-même, n'est-ce pas? Depuis SQL Server 2005, je pensais que ce paramètre n'avait aucun effet tant qu'il ansi_warningsétait activé. (En 2000, les vues indexées ne seraient pas utilisées si elles n'étaient pas définies correctement)
Martin Smith
@Martin: Je n'ai aucune expérience directe de cela, mais je me souviens d'avoir lu des trucs récemment. Et trouver des réponses SO là-dessus. Cela peut aider OP, cela peut ne pas ... Edit: sqlblog.com/blogs/kalen_delaney/archive/2008/06/19/… soupir
gbn
J'ai lu des affirmations similaires sans ambiguïté sur SO. Je n'ai jamais rien vu qui me permettrait de le reproduire pour moi-même ni aucune explication logique pour expliquer pourquoi le arithabortparamètre devrait avoir une telle influence dramatique sur les performances, donc je suis un peu sceptique à ce sujet pour le moment.
Martin Smith
ARITHABORT, ANSI_WARNINGS, ANSI_PADDING et ANSI_NULL valent 1, le reste est NULL.
Jon of All Trades,
Pour info, je travaille entièrement dans SSMS, donc différents paramètres dans VS ou d'autres clients ne sont pas en cause.
Jon of All Trades,