Abbiamo una tabella da 2,2 GB in Postgres con 7.801.611 righe al suo interno. Stiamo aggiungendo una colonna uuid / guid e mi chiedo quale sia il modo migliore per popolare quella colonna (poiché vogliamo aggiungere un vincolo NOT NULL
).
Se capisco correttamente Postgres, un aggiornamento è tecnicamente una cancellazione e un inserimento, quindi questo è fondamentalmente la ricostruzione dellintera tabella da 2,2 gb. Inoltre abbiamo uno slave in esecuzione, quindi non vogliamo che rimanga indietro.
Esiste un modo migliore che scrivere uno script che lo popoli lentamente nel tempo?
Commenti
Risposta
Dipende molto dai dettagli della configurazione e dai requisiti.
Tieni presente che, a partire da Postgres 11, solo laggiunta di una colonna con un volatile DEFAULT
attiva ancora la riscrittura della tabella . Purtroppo questo è il tuo caso.
Se hai spazio libero sufficiente su disco – almeno il 110% di pg_size_pretty((pg_total_relation_size(tbl))
e può permettersi un blocco di condivisione per un po di tempo e un blocco esclusivo per brevissimo tempo, quindi crea un nuova tabella inclusa la colonna uuid
utilizzando CREATE TABLE AS
. Perché?
- Che cosa causa il rallentamento di INSERT di grandi dimensioni e lesplosione dellutilizzo del disco?
Il codice seguente utilizza una funzione dal uuid-oss
modulo aggiuntivo.
-
Blocca la tabella contro modifiche simultanee in
SHARE
modalità (consentendo comunque letture simultanee). I tentativi di scrivere nella tabella attenderanno e alla fine falliranno. Vedi sotto. -
Copia lintera tabella mentre popola la nuova colonna al volo, possibilmente ordinando le righe favorevolmente mentre ci sei.
If stai per riordinare le righe, assicurati di impostarework_mem
abbastanza alto per eseguire lordinamento nella RAM o altrettanto alto come ti puoi permettere (solo per la tua sessione, non a livello globale). -
Quindi aggiungi vincoli, chiavi esterne, indici, trigger ecc. al nuovo tavolo. Quando si aggiornano grandi porzioni di una tabella, è molto più veloce creare indici da zero che aggiungere righe iterativamente. Consigli correlati nel manuale.
-
Quando la nuova tabella è pronta, elimina la vecchia e rinomina la nuova per renderlo un sostituto immediato. Solo questultimo passaggio acquisisce un blocco esclusivo sulla vecchia tabella per il resto della transazione, che ora dovrebbe essere molto breve.
Richiede inoltre leliminazione di qualsiasi oggetto a seconda del tipo di tabella (viste, funzioni che utilizzano il tipo di tabella nella firma, …) e ricrearli in seguito. -
Fai tutto in una transazione per evitare stati incompleti.
BEGIN; LOCK TABLE tbl IN SHARE MODE; SET LOCAL work_mem = "???? MB"; -- just for this transaction CREATE TABLE tbl_new AS SELECT uuid_generate_v1() AS tbl_uuid, <list of all columns in order> FROM tbl ORDER BY ??; -- optionally order rows favorably while being at it. ALTER TABLE tbl_new ALTER COLUMN tbl_uuid SET NOT NULL , ALTER COLUMN tbl_uuid SET DEFAULT uuid_generate_v1() , ADD CONSTRAINT tbl_uuid_uni UNIQUE(tbl_uuid); -- more constraints, indices, triggers? DROP TABLE tbl; ALTER TABLE tbl_new RENAME tbl; -- recreate views etc. if any COMMIT;
Dovrebbe essere il più veloce. Qualsiasi altro metodo di aggiornamento in atto deve riscrivere anche lintera tabella, solo in un modo più costoso. Dovresti seguire questa strada solo se non hai abbastanza spazio libero su disco o non puoi permetterti di bloccare lintera tabella o generare errori per tentativi di scrittura simultanei.
Cosa succede alle scritture simultanee?
Altra transazione (in altre sessioni) che tenta di INSERT
/ UPDATE
/ DELETE
nella stessa tabella dopo che la transazione ha acquisito il blocco SHARE
, aspetterà fino a quando il blocco non viene rilasciato o si verifica un timeout, a seconda dellevento che si verifica per primo. >
fail in entrambi i casi, poiché la tabella su cui stavano cercando di scrivere è stata eliminata da sotto.
La nuova tabella ha una nuova OID della tabella, ma la transazione simultanea ha già risolto il nome della tabella nellOID della tabella precedente . Quando il blocco viene finalmente rilasciato, cercano di bloccare la tabella da soli prima di scrivere su di essa e lo scoprono ” è andato.Postgres risponderà:
ERROR: could not open relation with OID 123456
Dove 123456
è lOID della vecchia tabella. Devi rilevare leccezione e riprovare le query nel codice della tua app per evitarlo.
Se non puoi permetterti che ciò accada, devi mantenere la tua tabella originale.
Mantenere la tabella esistente, alternativa 1
Aggiorna in posizione (possibilmente eseguendo laggiornamento su piccoli segmenti alla volta) prima di aggiungere il vincolo NOT NULL
. Laggiunta di una nuova colonna con valori NULL e senza NOT NULL
vincolo costa poco.
Poiché Postgres 9.2 puoi anche creare un CHECK
con NOT VALID
:
Il vincolo continuerà essere applicato contro inserimenti o aggiornamenti successivi
Ciò ti consente di aggiornare le righe peu à peu – in più transazioni separate . Ciò evita di mantenere i blocchi di riga troppo a lungo e consente anche di riutilizzare le righe morte. (Dovrai eseguire VACUUM
manualmente se non cè abbastanza tempo intermedio per lavvio dellautovacuum.) Infine, aggiungi NOT NULL
vincolo e rimuovi il NOT VALID CHECK
vincolo:
ALTER TABLE tbl ADD CONSTRAINT tbl_no_null CHECK (tbl_uuid IS NOT NULL) NOT VALID; -- update rows in multiple batches in separate transactions -- possibly run VACUUM between transactions ALTER TABLE tbl ALTER COLUMN tbl_uuid SET NOT NULL; ALTER TABLE tbl ALTER DROP CONSTRAINT tbl_no_null;
Risposta correlata che discute di NOT VALID
in modo più dettagliato:
Mantieni la tabella esistente, alternativa 2
Prepara il nuovo stato in una tabella temporanea , TRUNCATE
loriginale e ricarica dalla tabella temporanea. Tutto in una transazione . Tu è ancora necessario eseguire un SHARE
lock prima pr eparing la nuova tabella per evitare di perdere scritture simultanee.
Dettagli in queste risposte correlate su SO:
- Il modo migliore per eliminare milioni di righe per ID
- Aggiungere una nuova colonna senza blocco tabella?
Commenti
- Risposta fantastica! Esattamente le informazioni che stavo cercando. Due domande 1. Hai qualche idea su un modo semplice per testare quanto tempo richiederebbe unazione come questa? 2. Se sono necessari, ad esempio, 5 minuti, cosa succede alle azioni che tentano di aggiornare una riga in quella tabella durante quei 5 minuti?
- @CollinPeters: 1. Il leone ‘ sarebbe impiegata per copiare la grande tabella e possibilmente ricreare indici e vincoli (dipende). Eliminare e rinominare è economico. Per testare, puoi eseguire lo script SQL preparato senza
LOCK
fino aDROP
escluso. Potevo solo pronunciare supposizioni selvagge e inutili. Per quanto riguarda 2., considera laddendum alla mia risposta. - @ErwinBrandstetter Continua a ricreare le viste, quindi se ho una dozzina di viste che usano ancora la vecchia tabella (oid) dopo la ridenominazione della tabella. Esiste un modo per eseguire la sostituzione profonda anziché rieseguire lintero aggiornamento / creazione della vista?
- @CodeFarmer: se rinomini una tabella, le viste continuano a funzionare con la tabella rinominata. Per fare in modo che le viste utilizzino la nuova tabella, è necessario ricrearle in base alla nuova tabella. (Anche per consentire leliminazione della vecchia tabella.) Nessun modo (pratico) per aggirarla.
- dal 9.2 postgres non ‘ t lancia
could not open relation with OID 123456
Risposta
Non ho una risposta” migliore “, ma Ho una risposta “meno negativa” che potrebbe consentirti di fare le cose in modo ragionevolmente veloce.
La mia tabella aveva 2 MM di righe e le prestazioni di aggiornamento stavano rallentando quando ho provato ad aggiungere una colonna timestamp secondaria che per impostazione predefinita era la prima .
ALTER TABLE mytable ADD new_timestamp TIMESTAMP ; UPDATE mytable SET new_timestamp = old_timestamp ; ALTER TABLE mytable ALTER new_timestamp SET NOT NULL ;
Dopo che è rimasto in sospeso per 40 minuti, lho provato su un piccolo lotto per avere unidea di quanto tempo potrebbe richiedere: la previsione era intorno 8 ore.
La risposta accettata è decisamente migliore, ma questa tabella è ampiamente utilizzata nel mio database. Ci sono alcune dozzine di tabelle in cui FKEY su di essa; Volevo evitare di cambiare CHIAVI ESTERE su così tante tabelle . E poi ci sono le visualizzazioni.
Un po di ricerca in documenti, case study e StackOverflow, e ho avuto il momento “A-Ha!”. non era nel core UPDATE, ma in tutte le operazioni INDEX. La mia tabella aveva 12 indici: alcuni per vincoli univoci, alcuni per accelerare il pianificatore di query e alcuni per la ricerca a testo completo.
Ogni riga che è stata AGGIORNATA non stava solo lavorando su CANCELLAZIONE / INSERIMENTO, ma anche il sovraccarico di alterare ogni indice e controllare i vincoli.
La mia soluzione era eliminare ogni indice e vincolo, aggiorna la tabella, quindi aggiungi nuovamente tutti gli indici / vincoli.
Ci sono voluti circa 3 minuti per scrivere una transazione SQL che eseguisse le seguenti operazioni:
- BEGIN;
- eliminati indici / vincoli
- aggiorna tabella
- aggiungi nuovamente indici / vincoli
- COMMIT;
Lesecuzione dello script ha richiesto 7 minuti.
La risposta accettata è decisamente migliore e più appropriata … ed elimina virtualmente la necessità di tempi di inattività. Nel mio caso, però, ci sarebbe voluto molto di più ” Lo sviluppatore “lavorava per utilizzare quella soluzione e avevamo un periodo di inattività pianificato di 30 minuti in cui poteva essere realizzato. La nostra soluzione lo ha affrontato in 10.
Commenti
- Per non parlare del fatto che consente le scritture simultanee. Le letture simultanee potrebbero essere lente senza indexe s comunque 🙂 Sarebbe interessante fare un benchmark confrontandolo con le altre opzioni …
ALTER TABLE .. ADD COLUMN ...
o devi rispondere anche a quella parte?