Perché Postgres UPDATE ha impiegato 39 ore?


17

Ho una tabella di Postgres con ~ 2,1 milioni di righe. Ho eseguito l'aggiornamento di seguito su di esso:

WITH stops AS (
    SELECT id,
           rank() OVER (ORDER BY offense_timestamp,
                     defendant_dl,
                     offense_street_number,
                     offense_street_name) AS stop
    FROM   consistent.master
    WHERE  citing_jurisdiction=1
)

UPDATE consistent.master
SET arrest_id=stops.stop
FROM stops
WHERE master.id = stops.id;

L'esecuzione della query ha richiesto 39 ore. Sto eseguendo questo su un processore portatile i7 Q720 con core 4 (fisico), molta RAM, nient'altro che esegue la maggior parte delle volte. Nessun vincolo di spazio sull'HDD. La tabella era stata recentemente aspirata, analizzata e reindicizzata.

Per tutto il tempo in cui la query era in esecuzione, almeno dopo il WITHcompletamento iniziale , l'utilizzo della CPU era generalmente basso e l'HDD era in uso al 100%. L'HDD veniva utilizzato così duramente che qualsiasi altra app veniva eseguita molto più lentamente del normale.

L'impostazione di alimentazione del laptop era ad alte prestazioni (Windows 7 x64).

Ecco l'esplosione:

Update on master  (cost=822243.22..1021456.89 rows=2060910 width=312)
  CTE stops
    ->  WindowAgg  (cost=529826.95..581349.70 rows=2060910 width=33)
          ->  Sort  (cost=529826.95..534979.23 rows=2060910 width=33)
                Sort Key: consistent.master.offense_timestamp, consistent.master.defendant_dl, consistent.master.offense_street_number, consistent.master.offense_street_name
                ->  Seq Scan on master  (cost=0.00..144630.06 rows=2060910 width=33)
                      Filter: (citing_jurisdiction = 1)
  ->  Hash Join  (cost=240893.51..440107.19 rows=2060910 width=312)
        Hash Cond: (stops.id = consistent.master.id)
        ->  CTE Scan on stops  (cost=0.00..41218.20 rows=2060910 width=48)
        ->  Hash  (cost=139413.45..139413.45 rows=2086645 width=268)
              ->  Seq Scan on master  (cost=0.00..139413.45 rows=2086645 width=268)

citing_jurisdiction=1esclude solo poche decine di migliaia di righe. Anche con quella WHEREclausola, sto ancora operando su oltre 2 milioni di righe.

Il disco rigido è interamente crittografato con TrueCrypt 7.1a. Questo rallenta un po 'le cose, ma non abbastanza per causare una query così tante ore.

Il WITH esecuzione della parte richiede solo circa 3 minuti.

Il arrest_id campo non aveva indice per la chiave esterna. In questa tabella sono presenti 8 indici e 2 chiavi esterne. Tutti gli altri campi nella query sono indicizzati.

Il arrest_idcampo non aveva vincoli tranneNOT NULL .

La tabella ha un totale di 32 colonne.

arrest_idè di tipo carattere variabile (20) . Mi rendo conto che rank()produce un valore numerico, ma devo usare i caratteri che variano (20) perché ho altre righe in cuiciting_jurisdiction<>1 usano dati non numerici per questo campo.

Il arrest_idcampo era vuoto per tutte le righe conciting_jurisdiction=1 .

Questo è un laptop personale di fascia alta (a partire da 1 anno fa). Sono l'unico utente. Non sono state eseguite altre query o operazioni. Il blocco sembra improbabile.

Non ci sono trigger da nessuna parte in questa tabella o in qualsiasi altra parte del database.

Altre operazioni su questo database non richiedono mai una quantità eccessiva di tempo. Con una corretta indicizzazione, le SELECTquery sono in genere abbastanza veloci.


Coloro che Seq Scansono un po 'paura ...
rogerdpack

Risposte:


18

Ho avuto qualcosa di simile accaduto di recente con una tabella di 3,5 milioni di righe. Il mio aggiornamento non finirà mai. Dopo molti esperimenti e frustrazioni, ho finalmente trovato il colpevole. Si è rivelato essere gli indici sulla tabella in fase di aggiornamento.

La soluzione era eliminare tutti gli indici sulla tabella da aggiornare prima di eseguire l'istruzione update. Una volta fatto ciò, l'aggiornamento è terminato in pochi minuti. Una volta completato l'aggiornamento, ho ricreato gli indici e sono tornato in attività. Questo probabilmente non ti aiuterà a questo punto, ma potrebbe essere qualcun altro alla ricerca di risposte.

Terrei gli indici sul tavolo da cui stai estraendo i dati. Quello non dovrà continuare ad aggiornare alcun indice e dovrebbe aiutare a trovare i dati che si desidera aggiornare. Funzionava bene su un laptop lento.


3
Sto cambiando la risposta migliore a te. Da quando ho pubblicato questo, ho riscontrato altre situazioni in cui gli indici sono il problema, anche se la colonna in fase di aggiornamento ha già un valore e non ha indice (!). Sembra che Postgres abbia un problema con il modo in cui gestisce gli indici su altre colonne. Non vi è alcun motivo per cui questi altri indici riducano il tempo di query di un aggiornamento quando l'unica modifica a una tabella è aggiornare una colonna non indicizzata e non si aumenta lo spazio allocato per nessuna riga di quella colonna.
Aren Cambre,

1
Grazie! Spero che aiuti gli altri. Mi avrebbe risparmiato ore di mal di testa per qualcosa di apparentemente molto semplice.
JC Avena,

5
@ArenCambre - c'è un motivo: PostgreSQL copia l'intera riga in una posizione diversa e contrassegna la vecchia versione come eliminata. Ecco come PostgreSQL implementa il controllo di concorrenza multi-versione (MVCC).
Piotr Findeisen,

La mia domanda è ... perché è il colpevole? Vedi anche stackoverflow.com/a/35660593/32453
rogerdpack

15

Il tuo problema più grande è fare enormi quantità di lavori pesanti e di scrittura su un disco rigido per laptop. Non sarà mai veloce, non importa quello che fai, soprattutto se è il tipo di unità 5400 RPM più lenta fornita in molti laptop.

TrueCrypt rallenta le cose più di "un po '" per le scritture. Le letture saranno ragionevolmente veloci, ma le scritture fanno sembrare RAID 5 veloce. L'esecuzione di un DB su un volume TrueCrypt sarà una tortura per le scritture, in particolare le scritture casuali.

In questo caso, penso che sprecheresti tempo cercando di ottimizzare la query. Stai riscrivendo la maggior parte delle righe in ogni caso, e sarà lento con la tua orribile situazione di scrittura. Quello che consiglierei è di:

BEGIN;
SELECT ... INTO TEMPORARY TABLE master_tmp ;
TRUNCATE TABLE consistent.master;
-- Now DROP all constraints on consistent.master, then:
INSERT INTO consistent.master SELECT * FROM master_tmp;
-- ... and re-create any constraints.

Ho il sospetto che sarà più veloce della semplice eliminazione e ricostruzione dei vincoli da soli, perché un AGGIORNAMENTO avrà schemi di scrittura abbastanza casuali che uccideranno la tua memoria. Due inserimenti in blocco, uno in una tabella non bloccata e uno in una tabella registrata WAL senza vincoli, probabilmente saranno più veloci.

Se disponi di backup assolutamente aggiornati e non ti dispiace dover ripristinare il database dai backup , puoi anche riavviare PostgreSQL con il fsync=offparametro e full_page_writes=off temporaneamente per questa operazione in blocco. Qualsiasi problema imprevisto come la perdita di potenza o un crash del sistema operativo lascerà il tuo database irrecuperabile mentre fsync=off.

L'equivalente di POSTGreSQL a "nessuna registrazione" è l'uso di tabelle non blog. Queste tabelle non bloccate vengono troncate se il DB si spegne in modo impuro mentre sono sporche. L'uso di tabelle non registrate dimezzerà almeno il carico di scrittura e ridurrà il numero di ricerche, in modo che possano essere MOLTO più veloci.

Come in Oracle, può essere una buona idea eliminare un indice e ricrearlo dopo un grosso aggiornamento batch. Il planner di PostgreSQL non riesce a capire che è in corso un grosso aggiornamento, mette in pausa gli aggiornamenti dell'indice, quindi ricostruisce l'indice alla fine; anche se potesse, sarebbe molto difficile capire a che punto vale la pena farlo, soprattutto in anticipo.


Questa risposta è perfetta per la grande quantità di scritture e la terribile perf di crittografia più lenta unità portatile. Vorrei anche notare che la presenza di 8 indici produce molte scritture extra e sconfigge l'applicabilità degli aggiornamenti di riga in blocco HOT , quindi la caduta di indici e l'utilizzo di un fattore di riempimento inferiore sulla tabella può impedire una tonnellata di migrazione di righe
dbenhur

1
Buona chiamata per aumentare le possibilità di HOT con un fattore di riempimento - anche se con TrueCrypt forzare i cicli di lettura e riscrittura dei blocchi in blocchi enormi non sono sicuro che aiuterà molto; la migrazione delle righe potrebbe anche essere più veloce perché la crescita della tabella sta almeno eseguendo blocchi di scrittura lineari.
Craig Ringer,

2,5 anni dopo sto facendo qualcosa di simile ma su un tavolo più grande. Solo per essere sicuri, è una buona idea eliminare tutti gli indici, anche se la singola colonna che sto aggiornando non è indicizzata?
Aren Cambre,

1
@ArenCambre In quel caso ... beh, è ​​complicato. Se la maggior parte dei tuoi aggiornamenti sarà idonea HOT, è meglio lasciare gli indici in posizione. In caso contrario, probabilmente vorrai rilasciare e ricreare. La colonna non è indicizzata, ma per poter eseguire un aggiornamento HOT è necessario anche spazio libero sulla stessa pagina, quindi dipende un po 'da quanto spazio morto c'è nella tabella. Se è per lo più in scrittura, direi di eliminare tutti gli indici. Se è un lotto aggiornato, potrebbe avere buchi e potresti essere OK. Strumenti come pageinspecte pg_freespacemappossono aiutare a determinarlo.
Craig Ringer,

Grazie. In questo caso, è una colonna booleana che aveva già una voce in ogni riga. Stavo cambiando la voce in alcune righe. Ho appena confermato: l'aggiornamento ha richiesto solo 2 ore dopo aver eliminato tutti gli indici. In precedenza, ho dovuto interrompere l'aggiornamento dopo 18 ore perché stava impiegando troppo tempo. Questo nonostante il fatto che la colonna che veniva aggiornata sicuramente non fosse indicizzata.
Aren Cambre,

2

Qualcuno darà una risposta migliore per Postgres, ma qui ci sono alcune osservazioni dal punto di vista di Oracle che possono essere applicate (ei commenti sono troppo lunghi per il campo dei commenti).

La mia prima preoccupazione sarebbe quella di provare ad aggiornare 2 milioni di righe in una transazione. In Oracle, dovresti scrivere un'immagine precedente di ciascun blocco in fase di aggiornamento in modo che l'altra sessione abbia ancora una lettura coerente senza leggere i blocchi modificati e hai la possibilità di eseguire il rollback. Questo è un lungo rollback in fase di sviluppo. Di solito è meglio fare le transazioni in piccoli pezzi. Pronuncia 1.000 record alla volta.

Se nella tabella sono presenti indici e la tabella verrà considerata fuori servizio durante la manutenzione, è spesso meglio rimuovere gli indici prima di un'operazione di grandi dimensioni e quindi ricrearli nuovamente in seguito. Più economico, quindi, costantemente cercando di mantenere gli indici con ogni record aggiornato.

Oracle consente suggerimenti "no logging" sulle istruzioni per interrompere il journaling. Accelera molto le dichiarazioni, ma lascia il tuo db in una situazione "irrecuperabile". Quindi si consiglia di eseguire il backup prima e di eseguire nuovamente il backup immediatamente dopo. Non so se Postgres abbia opzioni simili.


PostgreSQL non ha problemi con un lungo rollback, non esiste. ROLBACK è molto veloce in PostgreSQL, non importa quanto sia grande la tua transazione. Oracle! = PostgreSQL
Frank Heikens

@FrankHeikens Grazie, è interessante. Dovrò leggere come funziona il journaling su Postgres. Al fine di far funzionare l'intero concetto di transazioni, in qualche modo due diverse versioni dei dati devono essere mantenute durante una transazione, l'immagine precedente e l'immagine successiva e questo è il meccanismo a cui mi riferisco. In un modo o nell'altro, immagino che ci sia una soglia oltre la quale le risorse per mantenere la transazione saranno troppo costose.
Glenn,

2
@Glenn postgres mantiene le versioni di una riga nella tabella stessa - vedi qui per una spiegazione. Il compromesso è che ottieni tuple "morte" in giro, che vengono pulite in modo asincrono con quello che viene chiamato "vuoto" nei postgres (Oracle non ha bisogno di vuoto perché non ha mai file "morte" nella tabella stessa)
Jack dice prova topanswers.xyz il


@Glenn Il documento canonico per il controllo della concorrenza della versione della riga di PostgreSQL è postgresql.org/docs/current/static/mvcc-intro.html e vale la pena leggerlo. Vedi anche wiki.postgresql.org/wiki/MVCC . Si noti che MVCC con righe morte ed VACUUMè solo metà della risposta; PostgreSQL utilizza anche un cosiddetto "registro in anticipo" (in effetti un giornale) per fornire commit atomici e proteggere da scritture parziali, ecc. Vedi postgresql.org/docs/current/static/wal-intro.html
Craig Ringer
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.