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


39

TL; DR

Comme cette question continue de susciter l'intérêt, je vais résumer ici pour 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 me rends compte que cela ne concerne pas 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érons 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

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

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

Plan d'exécution réel

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

Boucles imbriquées (jointure interne)

Notez que les noms de table ne correspondent pas exactement parce que j'ai édité les noms de table de requête à des fins de lisibilité, mais il est assez facile de faire correspondre (ads_alt_category = category). Est-il possible 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 10 000 identificateurs identiques transmis à la procédure stockée.

Information additionnelle:

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

Modifier (résolu)

Comme le soulignait la réponse acceptée, le problème était la clause OR dans category_link JOIN. Cependant, le code suggéré dans la réponse acceptée est très lent, même plus lent que le code d'origine. Une solution beaucoup plus rapide et beaucoup plus propre consiste simplement à remplacer la condition JOIN actuelle par la suivante:

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

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


Nous aurons besoin de voir le reste du plan de requête.
RBarryYoung

Juste une remarque: avec autant de jointures dépendantes, des erreurs d'estimation de la cardinalité deviennent probables. Le plus souvent, les performances des requêtes sont perturbées par la sous-estimation de la cardinalité.
Usr

Le plan d'exécution propose-t-il des index? En outre, n'oubliez pas que vous pouvez définir des clés primaires et des index sur vos tables temporaires (plus d'informations ici )

@rbarry Si après avoir essayé les solutions actuelles, je n'ai rien, je vais améliorer la question

1
Que diriez-vous de dupliquer la requête avec UNION et de vous débarrasser du OU

Réponses:


17

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
)

ordans les conditions de jointure est toujours suspecte. 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 selectclause.


Avec la quantité de filtrage étant fait sur ce particulier rejoindre, je tester également changer le JOINun CROSS APPLYavec le INpassage à un EXISTSdans le APPLYl » WHEREarticle.

Merci Gordon, je vais tester cette première chose demain matin. @ Valverij, je ne suis pas familier avec l'application croisée, pourriez-vous décrire davantage votre solution, peut-être avec une réponse correcte, afin que je puisse voter s'il s'avère que ce serait le scénario le plus rapide?

3
J'accepte cette réponse parce que c'était la première réponse qui m'a mis en évidence le problème. La solution suggérée est cependant extrêmement lente, même plus lente que le code d'origine. Cependant, sachant que la clause OU était le problème, le simple fait de la remplacer par ON l.category_id = ISNULL(c5.category_id, c4.category_idl’astuce.
Luis Ferrao

1
@ LuisFerrao. . . Nous vous remercions de l'information supplémentaire. Il est utile de savoir que cela coalesce()pousse l'optimiseur dans la bonne direction.
Gordon Linoff

9

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 scinder en plusieurs jointures, vous pouvez également essayer une 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-value agit en tant qu'entrée droite et l'expression de table externe agit en tant qu'entrée gauche. L'entrée de droite est évaluée pour chaque ligne de l'entrée de gauche et les lignes produites sont combinées pour la sortie finale .

Fondamentalement, cela APPLYressemble à une sous-requête qui filtre d’abord les enregistrements de droite, puis les applique au reste de votre requête.

Cet article explique très bien ce que c'est et quand l'utiliser: http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

Il est important de noter, cependant, que le CROSS APPLYne fonctionne pas toujours plus vite qu'un INNER JOIN. Dans de nombreuses situations, ce sera probablement à peu près le même. Dans de rares cas, cependant, je l’ai effectivement vu ralentir (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 à une table avec trop de déclarations conditionnelles, alors j'ai tendance à me pencher vers APPLY

Aussi une note amusante: OUTER APPLYagira comme unLEFT JOIN

Aussi, s'il vous plaît prendre note de mon choix d'utiliser EXISTSplutôt que IN. Lorsque vous effectuez INune sous-requête, rappelez-vous qu’elle renverra l’ensemble des résultats, même après qu’il ait trouvé votre valeur. Avec EXISTS, cependant, il arrêtera la sous-requête dès qu'il trouvera une correspondance.


J'ai testé cette solution à fond. Comme vous l'avez écrit, c'est assez lent, mais vous avez oublié d'appliquer les conseils avec lesquels vous avez commencé votre message. Remplacer AND x.cat = c4.cat OR x.cat = c5.catpar x.cat = ISNULL(c5.cat, c4.cat)et supprimer la clause IN en a fait la deuxième solution la plus rapide et digne d'un vote positif, car elle est plutôt informative.
Luis Ferrao

Merci. La ligne IN n'était en fait pas supposée être là (je ne pouvais pas décider d'utiliser IN ou de coller avec l'OR), je vais l'enlever.
Valverij
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.