So optimieren Sie eine Abfrage, die in verschachtelten Schleifen langsam ausgeführt wird (Inner Join)


39

TL; DR

Da diese Frage immer wieder auftaucht, fasse ich sie hier zusammen, damit Neulinge die Geschichte nicht leiden müssen:

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

Mir ist klar, dass dies möglicherweise nicht jedermanns Problem ist, aber wenn Sie die Empfindlichkeit der ON-Klauseln hervorheben, hilft dies Ihnen möglicherweise, in die richtige Richtung zu schauen. In jedem Fall ist der Originaltext hier für zukünftige Anthropologen:

Original Text

Betrachten Sie die folgende einfache Abfrage (nur 3 betroffene Tabellen)

    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

Dies ist eine ziemlich einfache Abfrage. Der einzige verwirrende Teil ist der letzte Kategorie-Join. Dies ist der Fall, da Kategorie-Level 5 möglicherweise vorhanden ist oder nicht. Am Ende der Abfrage suche ich nach Kategoriedaten pro Produkt-ID (SKU-ID) und hier kommt die sehr große Tabelle category_link ins Spiel. Schließlich ist die Tabelle #Ids nur eine temporäre Tabelle mit 10'000 IDs.

Bei der Ausführung erhalte ich den folgenden tatsächlichen Ausführungsplan:

Tatsächlicher Ausführungsplan

Wie Sie sehen, werden fast 90% der Zeit in den verschachtelten Schleifen (Inner Join) verbracht. Hier sind zusätzliche Informationen zu diesen verschachtelten Schleifen:

Verschachtelte Schleifen (Inner Join)

Beachten Sie, dass die Tabellennamen nicht genau übereinstimmen, da ich die Abfragetabellennamen aus Gründen der Lesbarkeit bearbeitet habe, die Zuordnung jedoch recht einfach ist (ads_alt_category = category). Gibt es eine Möglichkeit, diese Abfrage zu optimieren? Beachten Sie auch, dass in der Produktion die temporäre Tabelle #Ids nicht existiert, sondern ein Tabellenwertparameter mit denselben 10'000 IDs ist, die an die gespeicherte Prozedur weitergeleitet wurden.

Zusätzliche Information:

  • Kategorieindizes für category_id und parent_category_id
  • category_voc-Index für category_id, language_code
  • category_link-Index für sku_id, category_id

Bearbeiten (gelöst)

Wie aus der akzeptierten Antwort hervorgeht, war das Problem die OR-Klausel im category_link JOIN. Der in der akzeptierten Antwort vorgeschlagene Code ist jedoch sehr langsam und sogar langsamer als der ursprüngliche Code. Eine viel schnellere und auch viel sauberere Lösung besteht einfach darin, die aktuelle JOIN-Bedingung durch Folgendes zu ersetzen:

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

Diese minuziöse Optimierung ist die schnellste Lösung, die anhand der Doppelverknüpfung aus der akzeptierten Antwort und anhand der von valverij vorgeschlagenen KREUZANWENDUNG getestet wurde.


Wir müssen den Rest des Abfrageplans sehen.
RBarryYoung

Nur eine Bemerkung: Damit werden viele Fehler bei der Schätzung der Kardinalität von abhängigen Verknüpfungen wahrscheinlich. Am häufigsten wird die Abfrageleistung durch eine Unterschätzung der Kardinalität beeinträchtigt.
USR

Enthält der Ausführungsplan Vorschläge für Indizes? Auch vergessen Sie nicht , dass Sie Primärschlüssel und Indizes auf temporäre Tabellen (weitere Informationen einstellen können hier )

@rbarry Wenn ich nach dem Ausprobieren der aktuellen Lösungen nichts bekomme, werde ich die Frage verbessern

1
Was ist die Abfrage mit einer UNION duplizieren und das Loswerden der ODER

Antworten:


17

Das Problem scheint in diesem Teil des Codes zu liegen:

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
)

orin Join-Bedingungen ist immer verdächtig. Ein Vorschlag ist, dies in zwei Joins aufzuteilen:

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

Sie müssen dann den Rest der Abfrage ändern, um dies zu handhaben. . . coalesce(l1.sku_id, l2.sku_id)zum Beispiel in der selectKlausel.


Mit der Menge der Filterung an diesem getan beitreten, würde ich teste auch den Wechsel JOINauf einem CROSS APPLYmit der INUmschaltung auf ein EXISTSin der APPLY‚s - WHEREKlausel.

Danke Gordon, ich werde das gleich morgen früh testen. @Valverij, ich kenne mich nicht mit Cross Apply aus. Könnten Sie Ihre Lösung genauer beschreiben, vielleicht in einer richtigen Antwort, damit ich abstimmen kann, ob sie sich als das schnellste Szenario herausstellt?

3
Ich akzeptiere diese Antwort, weil es die erste war, die mich auf das Problem hinwies. Die vorgeschlagene Lösung ist jedoch extrem langsam und sogar langsamer als der ursprüngliche Code. Zu wissen, dass die OR-Klausel das Problem war, hat jedoch einfach geholfen, sie durch zu ersetzen ON l.category_id = ISNULL(c5.category_id, c4.category_id.
Luis Ferrao

1
@ LuisFerrao. . . Vielen Dank für die zusätzlichen Informationen. Es ist nützlich zu wissen, dass coalesce()der Optimierer in die richtige Richtung bewegt wird.
Gordon Linoff

9

Wie ein anderer Benutzer bereits erwähnt hat, ist dieser Join wahrscheinlich die Ursache:

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
)

Sie können diese nicht nur in mehrere Joins aufteilen, sondern auch versuchen, a 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

Über den obigen MSDN-Link:

Die Tabellenwertfunktion fungiert als rechte Eingabe und der äußere Tabellenausdruck als linke Eingabe. Die rechte Eingabe wird für jede Zeile von der linken Eingabe ausgewertet und die erzeugten Zeilen werden für die endgültige Ausgabe kombiniert .

Im Grunde APPLYist es wie eine Unterabfrage, bei der Datensätze zuerst rechts herausgefiltert und dann auf den Rest Ihrer Abfrage angewendet werden.

In diesem Artikel wird sehr gut erklärt, was es ist und wann es verwendet werden soll: http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

Es ist jedoch wichtig zu beachten, dass die CROSS APPLYnicht immer schneller als eine INNER JOIN. In vielen Situationen wird es wahrscheinlich ungefähr gleich sein. In seltenen Fällen habe ich es jedoch tatsächlich langsamer gesehen (dies hängt wiederum von Ihrer Tabellenstruktur und der Abfrage selbst ab).

Als allgemeine Faustregel neige ich dazu, mich zu neigen, wenn ich mit viel zu vielen bedingten Anweisungen an einen Tisch gehe APPLY

Auch ein lustiger Hinweis: OUTER APPLYwird sich wie einLEFT JOIN

Bitte beachten Sie auch meine Wahl zu verwenden, EXISTSanstatt IN. Wenn dabei INauf einer Unterabfrage, denken Sie daran , dass es die gesamte Ergebnismenge zurück, selbst nachdem es Ihren Wert gefunden hat. Mit EXISTSwird die Unterabfrage jedoch angehalten, sobald eine Übereinstimmung gefunden wird.


Ich habe diese Lösung gründlich getestet. Wie Sie es geschrieben haben, ist es ziemlich langsam, aber Sie haben vergessen, den Ratschlag anzuwenden, mit dem Sie Ihre Nachricht begonnen haben. Das Ersetzen AND x.cat = c4.cat OR x.cat = c5.catdurch x.cat = ISNULL(c5.cat, c4.cat)und das Entfernen der IN-Klausel machten dies zur zweitschnellsten Lösung und verdienen eine Aufwertung, da es ziemlich informativ ist.
Luis Ferrao

Vielen Dank. Die IN-Leitung sollte eigentlich nicht vorhanden sein (ich konnte mich nicht für die Verwendung von IN entscheiden oder mich an den OP halten), ich werde sie entfernen.
Valverij
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.