web-dev-qa-db-fra.com

Comment optimiser une requête qui s'exécute lentement sur les boucles imbriquées (jointure interne)

TL; DR

Étant donné que cette question continue de recevoir des opinions, je vais la résumer ici afin que les nouveaux arrivants n'aient pas à subir l'histoire:

JOIN table t ON t.member = @value1 OR t.member = @value2 -- this is slow as hell
JOIN table t ON t.member = COALESCE(@value1, @value2)    -- this is blazing fast
-- Note that here if @value1 has a value, @value2 is NULL, and vice versa

Je sais que ce n'est peut-être pas le problème de tout le monde, mais en soulignant la sensibilité des clauses ON, cela pourrait vous aider à regarder dans la bonne direction. En tout cas le texte original est là pour les futurs anthropologues:

Texte original

Considérez la requête simple suivante (seulement 3 tables impliquées)

    SELECT

        l.sku_id AS ProductId,
        l.is_primary AS IsPrimary,
        v1.category_name AS Category1,
        v2.category_name AS Category2,
        v3.category_name AS Category3,
        v4.category_name AS Category4,
        v5.category_name AS Category5

    FROM category c4
    JOIN category_voc v4 ON v4.category_id = c4.category_id and v4.language_code = 'en'

    JOIN category c3 ON c3.category_id = c4.parent_category_id
    JOIN category_voc v3 ON v3.category_id = c3.category_id and v3.language_code = 'en'

    JOIN category c2 ON c2.category_id = c3.category_id
    JOIN category_voc v2 ON v2.category_id = c2.category_id and v2.language_code = 'en'

    JOIN category c1 ON c1.category_id = c2.parent_category_id
    JOIN category_voc v1 ON v1.category_id = c1.category_id and v1.language_code = 'en'

    LEFT OUTER JOIN category c5 ON c5.parent_category_id = c4.category_id
    LEFT OUTER JOIN category_voc v5 ON v5.category_id = c5.category_id and v5.language_code = @lang

    JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
    (
        l.category_id = c4.category_id OR
        l.category_id = c5.category_id
    )

    WHERE c4.[level] = 4 AND c4.version_id = 5

Il s'agit d'une requête assez simple, la seule partie déroutante est la dernière jointure de catégorie, c'est ainsi parce que le niveau de catégorie 5 peut ou non exister. À la fin de la requête, je recherche des informations de catégorie par ID de produit (SKU ID), et c'est là que le très grand tableau category_link entre en jeu. Enfin, la table #Ids est juste une table temporaire contenant 10'000 Ids.

Une fois exécuté, j'obtiens le plan d'exécution réel suivant:

Actual Execution Plan

Comme vous pouvez le voir, près de 90% du temps est consacré aux boucles imbriquées (jointure interne). Voici des informations supplémentaires sur ces boucles imbriquées:

Nested Loops (Inner Join)

Notez que les noms de table ne correspondent pas exactement parce que j'ai modifié les noms de table de requête pour plus de lisibilité, mais il est assez facile à faire correspondre (ads_alt_category = category). Existe-t-il un moyen d'optimiser cette requête? Notez également qu'en production, la table temporaire #Ids n'existe pas, il s'agit d'un paramètre de valeur de table des mêmes 10'000 ID transmis à la procédure stockée.

Information additionnelle:

  • indices de catégorie sur category_id et parent_category_id
  • index category_voc sur category_id, language_code
  • index category_link sur sku_id, category_id

Modifier (résolu)

Comme indiqué par la réponse acceptée, le problème était la clause OR dans le JOIN category_link. Cependant, le code suggéré dans la réponse acceptée est très lent, plus lent que le code original. Beaucoup une solution plus rapide et beaucoup plus propre consiste simplement à remplacer la condition JOIN actuelle par ce qui suit:

JOIN category_link l on l.sku_id IN (SELECT value FROM @p1) AND l.category_id = COALESCE(c5.category_id, c4.category_id)

Cette minute, Tweak est la solution la plus rapide, testée contre la double jointure de la réponse acceptée et également testée contre CROSS APPLY comme suggéré par valverij.

40
Luis Ferrao

Le problème semble être dans cette partie du code:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

or dans les conditions de jointure est toujours suspect. Une suggestion est de diviser cela en deux jointures:

JOIN category_link l1 on l1.sku_id in (SELECT value FROM #Ids) and l1.category_id = cr.category_id
left outer join
category_link l1 on l2.sku_id in (SELECT value FROM #Ids) and l2.category_id = cr.category_id

Vous devez ensuite modifier le reste de la requête pour gérer cela. . . coalesce(l1.sku_id, l2.sku_id) par exemple dans la clause select.

17
Gordon Linoff

Comme un autre utilisateur l'a mentionné, cette jointure est probablement la cause:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

En plus de les diviser en plusieurs jointures, vous pouvez également essayer un CROSS APPLY

CROSS APPLY (
    SELECT [some column(s)]
    FROM category_link x
    WHERE EXISTS(SELECT value FROM #Ids WHERE value = x.sku_id)
    AND (x.category_id = c4.category_id OR x.category_id = c5.category_id)        
) l

À partir du lien MSDN ci-dessus:

La fonction table correspond à l'entrée droite et l'expression de table externe à l'entrée gauche. L'entrée droite est évaluée pour chaque ligne à partir de l'entrée gauche et les lignes produites sont combinées pour la sortie finale .

Fondamentalement, APPLY est comme une sous-requête qui filtre d'abord les enregistrements à droite, et puis les applique au reste de votre requête.

Cet article explique très bien de quoi il s'agit et quand l'utiliser: http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

Il est toutefois important de noter que le CROSS APPLY ne fonctionne pas toujours plus vite qu'un INNER JOIN. Dans de nombreuses situations, il en sera probablement de même. Dans de rares cas, cependant, je l'ai vu plus lentement (encore une fois, tout dépend de la structure de votre table et de la requête elle-même).

En règle générale, si je me retrouve à rejoindre une table avec beaucoup trop d'instructions conditionnelles, j'ai tendance à me pencher vers APPLY

Aussi une note amusante: OUTER APPLY agira comme un LEFT JOIN

Veuillez également noter mon choix d'utiliser EXISTS plutôt que IN. Lorsque vous effectuez IN sur une sous-requête, n'oubliez pas qu'il renverra l'ensemble de résultats, même après avoir trouvé votre valeur. Avec EXISTS, cependant, il arrêtera la sous-requête dès qu'il trouvera une correspondance.

9
valverij