Ottimizzazione del join su un tavolo di grandi dimensioni


10

Sto cercando di convincere un po 'di più le prestazioni di una query che accede a una tabella con circa 250 milioni di record. Dalla mia lettura del piano di esecuzione effettivo (non stimato), il primo collo di bottiglia è una query simile a questa:

select
    b.stuff,
    a.added,
    a.value
from
    dbo.hugetable a
    inner join
    #smalltable b on a.fk = b.pk
where
    a.added between @start and @end;

Vedi più in basso per le definizioni delle tabelle e degli indici coinvolti.

Il piano di esecuzione indica che un ciclo nidificato viene utilizzato su #smalltable e che la scansione dell'indice su hugetable viene eseguita 480 volte (per ogni riga in #smalltable). Questo mi sembra arretrato, quindi ho cercato di forzare invece un merge join da utilizzare:

select
    b.stuff,
    a.added,
    a.value
from
    dbo.hugetable a with(index = ix_hugetable)
    inner merge join
    #smalltable b with(index(1)) on a.fk = b.pk
where
    a.added between @start and @end;

L'indice in questione (vedi sotto per la definizione completa) copre le colonne fk (predicato del join), aggiunte (usate nella clausola where) e id (inutili) in ordine crescente e include valore .

Quando lo faccio, tuttavia, la query esplode da 2 1/2 minuti a oltre 9. Avrei sperato che i suggerimenti avrebbero forzato un join più efficiente che fa solo un singolo passaggio su ogni tabella, ma chiaramente no.

Qualsiasi consiglio è il benvenuto. Ulteriori informazioni fornite se necessario.

Aggiornamento (2011/06/02)

Dopo aver riorganizzato l'indicizzazione sul tavolo, ho fatto progressi significativi in ​​termini di prestazioni, tuttavia ho colto un nuovo ostacolo quando si tratta di riassumere i dati nella tabella enorme. Il risultato è un riepilogo per mese, che attualmente appare come il seguente:

select
    b.stuff,
    datediff(month, 0, a.added),
    count(a.value),
    sum(case when a.value > 0 else 1 end) -- this triples the running time!
from
    dbo.hugetable a
    inner join
    #smalltable b on a.fk = b.pk
group by
    b.stuff,
    datediff(month, 0, a.added);

Al momento, hugetable ha un indice cluster pk_hugetable (added, fk)(la chiave primaria) e un indice non cluster che va nell'altra direzione ix_hugetable (fk, added).

Senza la quarta colonna sopra, l'ottimizzatore utilizza un join di loop nidificato come prima, utilizzando #smalltable come input esterno e un indice non cluster cerca come loop interno (eseguendo nuovamente 480 volte). Ciò che mi preoccupa è la disparità tra le file stimate (12.958,4) e quelle effettive (74.668.468). Il costo relativo di queste ricerche è del 45%. Il tempo di esecuzione è tuttavia inferiore a un minuto.

Con la quarta colonna, il tempo di esecuzione raggiunge i 4 minuti. Questa volta cerca sull'indice cluster (2 esecuzioni) per lo stesso costo relativo (45%), aggrega tramite una corrispondenza hash (30%), quindi esegue un join hash su #smalltable (0%).

Non sono sicuro del mio prossimo corso d'azione. La mia preoccupazione è che né la ricerca per intervallo di date né il predicato di join siano garantiti o anche con tutta probabilità di ridurre drasticamente il set di risultati. L'intervallo di date nella maggior parte dei casi taglierà solo il 10-15% dei record e il join interno su fk potrebbe filtrare forse il 20-30%.


Come richiesto da Will A, i risultati di sp_spaceused:

name      | rows      | reserved    | data        | index_size  | unused
hugetable | 261774373 | 93552920 KB | 18373816 KB | 75167432 KB | 11672 KB

#smalltable è definito come:

create table #endpoints (
    pk uniqueidentifier primary key clustered,
    stuff varchar(6) null
);

Mentre dbo.hugetable è definito come:

create table dbo.hugetable (
    id uniqueidentifier not null,
    fk uniqueidentifier not null,
    added datetime not null,
    value decimal(13, 3) not null,

    constraint pk_hugetable primary key clustered (
        fk asc,
        added asc,
        id asc
    )
    with (
        pad_index = off, statistics_norecompute = off,
        ignore_dup_key = off, allow_row_locks = on,
        allow_page_locks = on
    )
    on [primary]
)
on [primary];

Con il seguente indice definito:

create nonclustered index ix_hugetable on dbo.hugetable (
    fk asc, added asc, id asc
) include(value) with (
    pad_index = off, statistics_norecompute = off,
    sort_in_tempdb = off, ignore_dup_key = off,
    drop_existing = off, online = off,
    allow_row_locks = on, allow_page_locks = on
)
on [primary];

Il campo ID è ridondante, un artefatto di un precedente DBA che ha insistito sul fatto che tutte le tabelle dovessero avere un GUID, senza eccezioni.


Potresti includere il risultato di 'dbo.hugetable' sp_spaceused, per favore?
Sarà un

Fatto, aggiunto appena sopra l'inizio delle definizioni della tabella.
Rapido Joe Smith,

Lo è di sicuro. Le sue dimensioni ridicole sono il motivo per cui sto esaminando questo.
Rapido Joe Smith,

Risposte:


5

Il tuo ix_hugetableaspetto è abbastanza inutile perché:

  • esso è l'indice cluster (PK)
  • INCLUDE non fa alcuna differenza perché un indice cluster INCLUDE tutte le colonne non chiave (valori non chiave al foglio più basso = INCLUDEd = cos'è un indice cluster)

Inoltre: - aggiunto o fk dovrebbe essere il primo - ID è il primo = poco uso

Prova a cambiare (added, fk, id)e rilasciare la chiave cluster ix_hugetable. Hai già provato (fk, added, id). Se non altro, risparmierai molto spazio su disco e la manutenzione dell'indice

Un'altra opzione potrebbe essere quella di provare il suggerimento FORCE ORDER con i modi di ordine delle tabelle e nessun suggerimento JOIN / INDEX. Cerco di non utilizzare personalmente i suggerimenti JOIN / INDEX perché rimuovi le opzioni per l'ottimizzatore. Molti anni fa mi è stato detto (seminario con un guru SQL) che il suggerimento di FORCE ORDER può essere d'aiuto quando si dispone di un enorme tavolo JOIN small table: YMMV 7 anni dopo ...

Oh, e facci sapere dove abita il DBA in modo da poter organizzare alcuni aggiustamenti di percussioni

Modifica, dopo l'aggiornamento del 02 giugno

La quarta colonna non fa parte dell'indice non cluster quindi utilizza l'indice cluster.

Prova a modificare l'indice NC in INCLUDI la colonna del valore in modo che non debba accedere alla colonna del valore per l'indice cluster

create nonclustered index ix_hugetable on dbo.hugetable (
    fk asc, added asc
) include(value)

Nota: se il valore non è nulla, è uguale a COUNT(*)semanticamente. Ma per SUM ha bisogno del valore reale , non dell'esistenza .

Ad esempio, se si COUNT(value)passa a COUNT(DISTINCT value) senza modificare l'indice, è necessario interrompere nuovamente la query perché deve elaborare il valore come valore, non come esistenza.

La query richiede 3 colonne: aggiunto, fk, valore. I primi 2 sono filtrati / uniti così come le colonne chiave. il valore è appena usato, quindi può essere incluso. Uso classico di un indice di copertura.


Ah, avevo in testa che gli indici cluster e non cluster avevano fk e aggiunto in ordine diverso. Non riesco a credere di non essermene accorto, quasi quanto non riesco a credere che sia stato impostato in questo modo in primo luogo. Domani cambierò l'indice cluster, poi andrò in strada per un caffè mentre si ricostruisce.
Rapido Joe Smith,

Ho modificato l'indicizzazione e ho avuto una bash con FORCE ORDER nel tentativo di ridurre il numero di ricerche sul tavolo di grandi dimensioni ma senza risultati. La mia domanda è stata aggiornata
Rapido Joe Smith,

@Quick Joe Smith: aggiornata la mia risposta
gbn

Sì, ci ho provato non molto tempo dopo. Poiché la ricostruzione dell'indice richiede così tanto tempo, me ne sono dimenticata e inizialmente pensavo di averlo accelerato facendo qualcosa di completamente indipendente.
Rapido Joe Smith,

2

Definisci un indice hugetablesolo sulla addedcolonna.

I DB useranno un indice multiparte (multi colonna) solo fino all'estrema destra dell'elenco delle colonne in quanto ha valori che contano da sinistra. La tua query non specifica fknella clausola where della prima query, quindi ignora l'indice.


Il piano di esecuzione mostra che si sta cercando l'indice (ix_hugetable) . O stai dicendo che questo indice non è appropriato per la query?
Rapido Joe Smith,

L'indice non è appropriato. Chissà come sta "usando l'indice". L'esperienza mi dice che questo è il tuo problema. Provalo e dicci come va.
Boemia,

@Quick Joe Smith - hai provato il suggerimento di Bohemian? Quali sono i risultati?
Lieven Keersmaekers,

2
Non sono d'accordo: la clausola ON viene prima elaborata logicamente ed è effettivamente un WHERE in pratica, quindi OP deve provare prima entrambe le colonne. Nessuna indicizzazione su fk affatto = scansione indice cluster o ricerca chiavi per ottenere il valore fk per JOIN. Puoi aggiungere alcuni riferimenti al comportamento che hai descritto anche per favore? Soprattutto per SQL Server, dato che hai una cronologia precedente che risponde a questo RDBMS. In realtà, -1 in retrospettiva mentre scrivo questo commento
gbn

2

Il piano di esecuzione indica che un ciclo nidificato viene utilizzato su #smalltable e che la scansione dell'indice su hugetable viene eseguita 480 volte (per ogni riga in #smalltable).

Questo è l'ordine che mi aspetto che venga utilizzato da Query Optimizer, supponendo che un loop si unisca nella scelta giusta. L'alternativa è di ripetere 250 M volte ed eseguire una ricerca nella tabella #temp ogni volta, il che potrebbe richiedere ore / giorni.

L'indice che stai forzando a utilizzare nell'unione MERGE è praticamente di 250 milioni di righe * 'la dimensione di ogni riga' - non piccola, almeno un paio di GB. A giudicare sp_spaceuseddall'output "un paio di GB" potrebbe essere un eufemismo: il join MERGE richiede che si passi attraverso l'indice che sarà molto intensivo per l'I / O.


La mia comprensione è che esistono 3 tipi di algoritmi di join e che il join di unione ha le migliori prestazioni quando entrambi gli input sono ordinati dal predicato di join. Giustamente o erroneamente, questo è il risultato che sto cercando di ottenere.
Rapido Joe Smith,

2
Ma c'è di più. Se #smalltable aveva un numero elevato di righe, un join di unione potrebbe essere appropriato. Se, come suggerisce il nome, ha un piccolo numero di righe, un loop loop potrebbe essere la scelta giusta. Immagina che #smalltable avesse una o due righe e corrispondesse a una manciata di righe dell'altra tabella: sarebbe difficile giustificare un join di unione qui.
Sarà un

Ho pensato che ci fosse dell'altro; Non sapevo cosa potesse essere. L'ottimizzazione del database non è esattamente il mio punto di forza, come probabilmente hai già intuito.
Rapido Joe Smith,

@Quick Joe Smith - grazie per sp_spaceused. 75 GB di indice e 18 GB di dati: ix_hugetable non è l'unico indice sul tavolo?
Will A

1
+1 Volontà. Il pianificatore sta attualmente facendo la cosa giusta. Il problema risiede nelle ricerche casuali su disco a causa del modo in cui le tabelle sono raggruppate.
Denis de Bernardy,

1

Il tuo indice non è corretto. Vedi gli indici dos e donts .

Allo stato attuale, l'unico indice utile è quello sulla chiave primaria della tabella piccola. L'unico piano ragionevole è quindi seq scansionare il tavolino e annidare il pasticcio con quello enorme.

Prova ad aggiungere un indice cluster su hugetable(added, fk). Ciò dovrebbe indurre il pianificatore a cercare le righe applicabili dall'enorme tabella e annidare il loop o unirle con la tabella piccola.


Grazie per quel link. Ci proverò quando domani lavorerò.
Rapido Joe Smith,
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.