Aggiornamento efficiente di una tabella tramite JOIN


8

Ho una tabella che ha i dettagli delle famiglie e un'altra che ha i dettagli di tutte le persone associate alle famiglie. Per la tabella delle famiglie ho una chiave primaria definita usando due colonne in essa - [tempId,n]. Per la tabella delle persone ho una chiave primaria definita usando 3 delle sue colonne[tempId,n,sporder]

Utilizzando l'ordinamento dettato dall'indicizzazione in cluster su chiavi primarie, ho generato un ID univoco per ogni famiglia [HHID]e [PERID]record di ogni persona (lo snippet di seguito è per la generazione di PERID]:

 ALTER TABLE dbo.persons
 ADD PERID INT IDENTITY
 CONSTRAINT [UQ dbo.persons HHID] UNIQUE;

Ora, il mio prossimo passo è quello di associare ogni persona alle famiglie corrispondenti cioè; mappare a [PERID]a a [HHID]. Il passaggio pedonale tra i due tavoli si basa sulle due colonne [tempId,n]. Per questo ho la seguente dichiarazione join interna.

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n;

Ho un totale di 1928783 documenti familiari e 5239842 documenti personali. Il tempo di esecuzione è attualmente molto elevato.

Ora, le mie domande:

  1. È possibile ottimizzare ulteriormente questa query? Più in generale, quali sono le regole del pollice per l'ottimizzazione di una query di join?
  2. Esiste un altro costrutto di query che può ottenere il risultato desiderato con tempi di esecuzione migliori?

Ho caricato il piano di esecuzione generato da SQL Server 2008 per l'intero script su SQLPerformance.com

Risposte:


19

Sono abbastanza sicuro che le definizioni della tabella sono vicine a questo:

CREATE TABLE dbo.households
(
    tempId  integer NOT NULL,
    n       integer NOT NULL,
    HHID    integer IDENTITY NOT NULL,

    CONSTRAINT [UQ dbo.households HHID] 
        UNIQUE NONCLUSTERED (HHID),

    CONSTRAINT [PK dbo.households tempId, n]
    PRIMARY KEY CLUSTERED (tempId, n)
);

CREATE TABLE dbo.persons
(
    tempId  integer NOT NULL,
    sporder integer NOT NULL,
    n       integer NOT NULL,
    PERID   integer IDENTITY NOT NULL,
    HHID    integer NOT NULL,

    CONSTRAINT [UQ dbo.persons HHID]
        UNIQUE NONCLUSTERED (PERID),

    CONSTRAINT [PK dbo.persons tempId, n, sporder]
        PRIMARY KEY CLUSTERED (tempId, n, sporder)
);

Non ho statistiche per queste tabelle o i tuoi dati, ma quanto segue imposterà almeno la cardinalità della tabella corretta (i conteggi delle pagine sono un'ipotesi):

UPDATE STATISTICS dbo.persons 
WITH 
    ROWCOUNT = 5239842, 
    PAGECOUNT = 100000;

UPDATE STATISTICS dbo.households 
WITH 
    ROWCOUNT = 1928783, 
    PAGECOUNT = 25000;

Analisi del piano di query

La query che hai ora è:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n;

Questo genera il piano piuttosto inefficiente:

Piano predefinito

I problemi principali in questo piano sono l'hash join e l'ordinamento. Entrambi richiedono una concessione di memoria (il join hash deve creare una tabella hash e l'ordinamento ha bisogno di spazio per memorizzare le righe mentre l'ordinamento avanza). Plan Explorer mostra che a questa query sono stati concessi 765 MB:

Memory Grant

Questa è molta memoria del server da dedicare a una query! Più precisamente, questa concessione di memoria viene risolta prima che inizi l'esecuzione in base al conteggio delle righe e alle stime delle dimensioni.

Se la memoria risulta insufficiente al momento dell'esecuzione, almeno alcuni dati per l'hash e / o l'ordinamento verranno scritti sul disco tempdb fisico . Questo è noto come "fuoriuscita" e può essere un'operazione molto lenta. È possibile tracciare questi sversamenti (in SQL Server 2008) utilizzando il profiler eventi Avvertenze Hash e Ordina avvertenze .

La stima per l'input di compilazione della tabella hash è molto buona:

Hash Input

La stima per l'input di ordinamento è meno accurata:

Ordina input

Dovresti usare Profiler per verificare, ma sospetto che l'ordinamento si riverserà su tempdb in questo caso. È anche possibile che la tabella hash si rovesci, ma è meno chiara.

Si noti che la memoria riservata per questa query viene suddivisa tra la tabella hash e l'ordinamento, poiché vengono eseguiti contemporaneamente. La proprietà del piano Frazioni di memoria mostra la quantità relativa della concessione di memoria che si prevede sarà utilizzata da ciascuna operazione.

Perché ordinare e hash?

L'ordinamento viene introdotto da Query Optimizer per garantire che le righe arrivino all'operatore Aggiornamento indice cluster in ordine di chiavi cluster. Ciò promuove l'accesso sequenziale alla tabella, che è spesso molto più efficiente dell'accesso casuale.

L'hash join è una scelta meno ovvia, perché i suoi input hanno dimensioni simili (comunque a una prima approssimazione). L'hash join è il migliore in cui un input (quello che crea la tabella hash) è relativamente piccolo.

In questo caso, il modello di costing dell'ottimizzatore determina che l'hash join è la più economica delle tre opzioni (hash, merge, loop nidificati).

Miglioramento delle prestazioni

Il modello di costo non sempre funziona correttamente. Tende a sovrastimare il costo dell'unione di unione parallela, soprattutto all'aumentare del numero di thread. Possiamo forzare un join di unione con un suggerimento per la query:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (MERGE JOIN);

Questo produce un piano che non richiede tanta memoria (poiché l'unione di join non ha bisogno di una tabella hash):

Unisci piano

L'ordinamento problematico è ancora presente, poiché l'unione unione mantiene solo l'ordine delle sue chiavi di unione (tempId, n) ma le chiavi del cluster sono (tempId, n, sporder). È possibile che il piano di unione unificata non sia migliore del piano di join hash.

Unisci loop annidati

Possiamo anche provare a unire loop nidificati:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (LOOP JOIN);

Il piano per questa query è:

Piano dei cicli nidificati seriali

Questo piano di query è considerato il peggiore dal modello di costing dell'ottimizzatore, ma ha alcune caratteristiche molto desiderabili. Innanzitutto, l'unione di cicli nidificati non richiede una concessione di memoria. In secondo luogo, può preservare l'ordine delle chiavi dalla Personstabella in modo che non sia necessario un ordinamento esplicito. Potresti scoprire che questo piano funziona relativamente bene, forse anche abbastanza bene.

Cicli annidati paralleli

Il grande svantaggio con il piano di cicli nidificati è che viene eseguito su un singolo thread. È probabile che questa query tragga vantaggio dal parallelismo, ma l'ottimizzatore decide che qui non c'è alcun vantaggio nel farlo. Anche questo non è necessariamente corretto. Sfortunatamente, non esiste un suggerimento di query integrato per ottenere un piano parallelo, ma esiste un modo non documentato:

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n
OPTION (LOOP JOIN, QUERYTRACEON 8649);

L'abilitazione del flag di traccia 8649 con il QUERYTRACEONsuggerimento produce questo piano:

Piano di cicli annidati paralleli

Ora abbiamo un piano che evita l'ordinamento, non richiede memoria aggiuntiva per il join e utilizza il parallelismo in modo efficace. Dovresti trovare che questa query funziona molto meglio delle alternative.

Maggiori informazioni sul parallelismo nel mio articolo Forcing a Parallel Query Execution Plan :


1

Esaminando il piano di query, è possibile che il problema reale non sia il join in sé, ma piuttosto il processo di aggiornamento effettivo.

Da quello che posso vedere, è probabile che tu stia aggiornando tutti i record delle persone nel tuo database e aggiorni gli indici (non riesco a vedere quali altri indici ha, quindi non so se potrebbe essere un fattore)

Se si tratta di un'attività una tantum, potresti disabilitare gli indici, eseguire l'aggiornamento e ricostruire gli indici?

Dopo aver popolato i dati, è possibile aggiungere una clausola where alla query per aggiornare solo i record che devono essere aggiornati

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.