Come ottimizzare una query che funziona lentamente su Nested Loops (Inner Join)


39

TL; DR

Poiché questa domanda continua a ricevere visualizzazioni, la riassumerò qui in modo che i nuovi arrivati ​​non debbano subire la storia:

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

Mi rendo conto che questo potrebbe non essere un problema per tutti, ma evidenziando la sensibilità delle clausole ON, potrebbe aiutarti a guardare nella giusta direzione. In ogni caso il testo originale è qui per i futuri antropologi:

Testo originale

Considera la seguente semplice query (coinvolte solo 3 tabelle)

    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

Questa è una query piuttosto semplice, l'unica parte confusa è l'ultimo join di categoria, è così perché il livello di categoria 5 potrebbe o non potrebbe esistere. Alla fine della query sto cercando informazioni sulla categoria per ID prodotto (SKU ID), ed è qui che entra in gioco la tabella molto grande category_link. Infine, la tabella #Ids è solo una tabella temporanea contenente 10'000 ID.

Quando eseguito, ottengo il seguente piano di esecuzione effettivo:

Piano di esecuzione effettivo

Come puoi vedere, quasi il 90% del tempo viene speso nei loop annidati (Inner Join). Ecco ulteriori informazioni su quei loop annidati:

Cicli nidificati (Inner Join)

Tieni presente che i nomi delle tabelle non corrispondono esattamente perché ho modificato i nomi delle tabelle delle query per renderle leggibili, ma è abbastanza facile abbinarli (ads_alt_category = categoria). Esiste un modo per ottimizzare questa query? Inoltre, in produzione, la tabella temporanea #Ids non esiste, è un parametro con valori di tabella degli stessi 10'000 ID passati alla Stored Procedure.

Informazioni addizionali:

  • indici di categoria su category_id e parent_category_id
  • indice category_voc su ID_categoria, codice_ lingua
  • indice category_link su sku_id, category_id

Modifica (risolto)

Come sottolineato dalla risposta accettata, il problema era la clausola OR nel JOIN di category_link. Tuttavia, il codice suggerito nella risposta accettata è molto lento, anche più lento del codice originale. Una soluzione molto più veloce e anche molto più pulita è semplicemente quella di sostituire l'attuale condizione JOIN con quanto segue:

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

Questa piccola modifica è la soluzione più veloce, testata contro il doppio join dalla risposta accettata e testata anche con CROSS APPLY come suggerito da valverij.


Dovremo vedere il resto del piano di query.
RBarryYoung,

Solo un'osservazione: con ciò molti errori dipendenti diventano probabili errori di stima della cardinalità. Molto spesso, le prestazioni delle query sono deragliate dalla sottostima della cardinalità.
usr

Il piano di esecuzione fornisce suggerimenti per gli indici? Inoltre, non dimenticare che puoi impostare chiavi primarie e indici sulle tue tabelle temporanee (maggiori informazioni qui )

@rbarry Se dopo aver provato le soluzioni attuali non ottengo nulla, migliorerò sulla domanda

1
Che ne dici di duplicare la query con un UNION e sbarazzarti dell'OR

Risposte:


17

Il problema sembra essere in questa parte del codice:

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 condizioni di partecipazione è sempre sospetto. Un suggerimento è di dividere questo in due join:

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

È quindi necessario modificare il resto della query per gestirlo. . . coalesce(l1.sku_id, l2.sku_id)per esempio nella selectclausola.


Con la quantità di filtro che viene eseguita su quel particolare join, testerei anche di cambiare JOINin a CROSS APPLYcon il INpassaggio a EXISTSin nella clausola di APPLYs WHERE.

Grazie Gordon, proverò questa prima cosa al mattino. @Valverij, non ho familiarità con l'applicazione incrociata, potresti descrivere di più la tua soluzione, magari in una risposta corretta, quindi posso votare se si rivela lo scenario più veloce?

3
Accetto questa risposta perché è stata la prima risposta a indicarmi il problema. La soluzione suggerita è tuttavia estremamente lenta, più lenta anche del codice originale. Tuttavia, sapendo che la clausola OR era il problema, semplicemente sostituendola con ON l.category_id = ISNULL(c5.category_id, c4.category_idil trucco.
Luis Ferrao,

1
@LuisFerrao. . . Grazie per le informazioni aggiuntive. È utile sapere che coalesce()spinge l'ottimizzatore nella giusta direzione.
Gordon Linoff,

9

Come menzionato da un altro utente, questo join è probabilmente la causa:

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
)

Oltre a dividerli in più join, puoi anche provare 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

Dal collegamento MSDN sopra:

La funzione con valori di tabella funge da input destro e l'espressione di tabella esterna funge da input sinistro. L'input destro viene valutato per ogni riga dall'input sinistro e le righe prodotte vengono combinate per l'output finale .

Fondamentalmente, APPLYè come una sottoquery che filtra prima i record a destra e quindi li applica al resto della query.

Questo articolo fa un ottimo lavoro nel spiegare di cosa si tratta e quando usarlo: http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

È importante notare, tuttavia, che CROSS APPLYnon funziona sempre più velocemente di un INNER JOIN. In molte situazioni, sarà probabilmente più o meno lo stesso. In rari casi, tuttavia, l'ho visto più lentamente (di nuovo, tutto dipende dalla struttura della tabella e dalla query stessa).

Come regola generale, se mi trovo a unirmi a un tavolo con troppe dichiarazioni condizionali, allora tendo a inclinarmi verso APPLY

Anche una nota divertente: OUTER APPLYagirà come unLEFT JOIN

Inoltre, prendi nota della mia scelta di utilizzare EXISTSpiuttosto che IN. Quando INesegui una query secondaria, ricorda che restituirà l'intero set di risultati, anche dopo aver trovato il tuo valore. Con EXISTS, tuttavia, interromperà la subquery nell'istante in cui trova una corrispondenza.


Ho testato a fondo questa soluzione. Mentre lo hai scritto, è piuttosto lento, ma hai dimenticato di applicare i consigli con cui hai iniziato il tuo messaggio. La sostituzione AND x.cat = c4.cat OR x.cat = c5.catda x.cat = ISNULL(c5.cat, c4.cat)e per liberarsi della clausola IN fatto questa la soluzione secondo più veloce, e degna di un upvote, perché è abbastanza informativo.
Luis Ferrao,

Grazie. La linea IN in realtà non avrebbe dovuto essere lì (non potevo decidere di usare IN o restare con l'OR), la rimuoverò.
valverij,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.