Il modo migliore per popolare una nuova colonna in una tabella di grandi dimensioni?

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

  • Hai già eseguito un ALTER TABLE .. ADD COLUMN ... o devi rispondere anche a quella parte?
  • Non hai eseguito eventuali modifiche alla tabella ancora, solo in fase di pianificazione. Lho fatto prima aggiungendo la colonna, popolandola, quindi aggiungendo il vincolo o lindice. Tuttavia, questa tabella è notevolmente più grande e sono preoccupato per il carico, il blocco, la replica, ecc. …

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é?

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 impostare work_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:

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 a DROP 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 …

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *