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


33

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 NOT NULLvincolo ad essa).

Se capisco correttamente Postgres, un aggiornamento è tecnicamente una cancellazione e inserimento, quindi sostanzialmente si tratta di ricostruire l'intera tabella da 2,2 GB. Inoltre abbiamo uno slave in esecuzione, quindi non vogliamo che rimanga indietro.

Esiste un modo migliore di scrivere una sceneggiatura che la popola lentamente nel tempo?


2
Hai già eseguito una ALTER TABLE .. ADD COLUMN ...o è necessario rispondere anche a quella parte?
ypercubeᵀᴹ

Non sono ancora state apportate modifiche alla tabella, solo in fase di pianificazione. L'ho già fatto aggiungendo la colonna, popolandola, quindi aggiungendo il vincolo o l'indice. Tuttavia, questa tabella è significativamente più grande e sono preoccupato per il carico, il blocco, la replica, ecc ...
Collin Peters,

Risposte:


45

Dipende molto dai dettagli delle tue esigenze.

Se si dispone di spazio libero sufficiente (almeno il 110% di pg_size_pretty((pg_total_relation_size(tbl))) sul disco e si può permettere un blocco della condivisione per un po 'di tempo e un blocco esclusivo per un tempo molto breve , quindi creare una nuova tabella includendo la uuidcolonna usando CREATE TABLE AS. Perché?

Il codice seguente utilizza una funzione dal uuid-ossmodulo aggiuntivo .

  • Blocca la tabella contro le modifiche simultanee in SHAREmodalità (consentendo comunque letture simultanee). I tentativi di scrivere sul tavolo aspetteranno e alla fine falliranno. Vedi sotto.

  • Copia l'intera tabella popolando al volo la nuova colonna, possibilmente ordinando le righe favorevolmente mentre ci sei.
    Se hai intenzione di riordinare le righe, assicurati di impostare work_memil massimo che puoi permetterti (solo per la tua sessione, non a livello globale).

  • Quindi aggiungere vincoli, chiavi esterne, indici, trigger ecc. Alla nuova tabella. Quando si aggiornano grandi porzioni di una tabella è molto più veloce creare indici da zero che aggiungere righe in modo iterativo.

  • Quando la nuova tabella è pronta, elimina la vecchia e rinomina la nuova per renderla una sostituzione drop-in. Solo quest'ultimo passaggio acquisisce un blocco esclusivo sulla vecchia tabella per il resto della transazione, che ora dovrebbe essere molto breve.
    Richiede inoltre di eliminare qualsiasi oggetto in base al tipo di tabella (viste, funzioni che utilizzano il tipo di tabella nella firma, ...) e di 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;

Questo dovrebbe essere il più veloce. Qualsiasi altro metodo di aggiornamento in atto deve riscrivere anche l'intera tabella, solo in un modo più costoso. Andresti su quella strada solo se non hai abbastanza spazio libero sul disco o non puoi permetterti di bloccare l'intera tabella o generare errori per tentativi di scrittura simultanei.

Cosa succede alle scritture simultanee?

Un'altra transazione (in altre sessioni) che tenta di INSERT/ UPDATE/ DELETEnella stessa tabella dopo che la transazione ha preso il SHAREblocco, attenderà fino al rilascio del blocco o al verificarsi di un timeout, a seconda dell'evento che si verifica per primo. Saranno fallire in entrambi i casi, dal momento che il tavolo che stavano cercando di scrivere è stato eliminato da sotto di loro.

La nuova tabella ha un nuovo OID di tabella, ma la transazione simultanea ha già risolto il nome della tabella nell'OID della tabella precedente . Quando il blocco viene finalmente rilasciato, provano a bloccare il tavolo da soli prima di scriverlo e scoprono che non c'è più. Postgres risponderà:

ERROR: could not open relation with OID 123456

Dov'è 123456l'OID della vecchia tabella. È necessario intercettare tale eccezione e riprovare le query nel codice dell'app per evitarlo.

Se non puoi permetterti che ciò accada, devi mantenere la tua tabella originale.

Due alternative che mantengono la tabella esistente

  1. Aggiornamento in atto (possibilmente eseguendo l'aggiornamento su piccoli segmenti alla volta) prima di aggiungere il NOT NULLvincolo. L'aggiunta di una nuova colonna con valori NULL e senza NOT NULLvincolo è economica.
    Da Postgres 9.2 puoi anche creare un CHECKvincolo conNOT VALID :

    Il vincolo verrà comunque applicato contro inserimenti o aggiornamenti successivi

    Ciò consente di aggiornare le righe peu à peu , in più transazioni separate . Questo evita di mantenere i blocchi delle file per troppo tempo e consente anche di riutilizzare le file morte. (Dovrai eseguire VACUUMmanualmente se non c'è abbastanza tempo in mezzo per avviare l'autovacuum.) Infine, aggiungi il NOT NULLvincolo e rimuovi il NOT VALID CHECKvincolo:

    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 discutendo NOT VALIDpiù in dettaglio:

  2. Preparare il nuovo stato in una tabella temporanea , TRUNCATEl'originale e riempire dalla tabella temporanea. Tutto in una transazione . È ancora necessario prendere un SHAREblocco prima di preparare la nuova tabella per evitare di perdere scritture simultanee.

    Dettagli in questa risposta correlata su SO:


Risposta fantastica! Esattamente le informazioni che stavo cercando. Due domande 1. Hai idea di un modo semplice per testare quanto tempo richiederebbe un'azione del genere? 2. Se bastano 5 minuti, cosa succede alle azioni che tentano di aggiornare una riga in quella tabella durante quei 5 minuti?
Collin Peters,

@CollinPeters: 1. La maggior parte del tempo andrebbe a copiare il grande tavolo - e forse a ricreare indici e vincoli (dipende). Eliminare e rinominare costa poco. Per testare è possibile eseguire lo script SQL preparato senza l' LOCKesclusione e l'esclusione di DROP. Potevo solo pronunciare ipotesi selvagge e inutili. Per quanto riguarda 2., si prega di considerare l'addendum alla mia risposta.
Erwin Brandstetter,

@ErwinBrandstetter Continua a ricreare le visualizzazioni, quindi se ho una dozzina di visualizzazioni che usano ancora la vecchia tabella (oid) dopo la ridenominazione della tabella. Esiste un modo per eseguire la sostituzione profonda anziché rieseguire l'intero aggiornamento / creazione della vista?
CodeFarmer

@CodeFarmer: se hai appena rinominato 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 l'eliminazione della vecchia tabella.) Nessun modo (pratico) per aggirarla.
Erwin Brandstetter,

14

Non ho una risposta "migliore", ma ho una risposta "meno male" che potrebbe farti fare le cose ragionevolmente velocemente.

La mia tabella aveva righe 2MM e le prestazioni di aggiornamento erano ridotte quando ho provato ad aggiungere una colonna timestamp secondaria che era passata alla 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 averlo sospeso per 40 minuti, l'ho provato su un piccolo lotto per farmi un'idea di quanto tempo ci sarebbe voluto: la previsione era di circa 8 ore.

La risposta accettata è decisamente migliore, ma questa tabella è ampiamente utilizzata nel mio database. Ci sono alcune decine di tabelle che FKEY su di esso; Volevo evitare di cambiare i TASTI ESTERI su così tanti tavoli. E poi ci sono viste.

Un po 'di ricerca di documenti, casi di studio e StackOverflow e ho avuto "A-Ha!" momento. Il drenaggio non era sul core UPDATE, ma su tutte le operazioni INDEX. La mia tabella aveva 12 indici: alcuni per vincoli univoci, alcuni per velocizzare il planner delle query e alcuni per la ricerca full-text.

Ogni riga AGGIORNATA non funzionava solo su DELETE / INSERT, ma anche sull'overhead di alterare ogni indice e controllare i vincoli.

La mia soluzione era eliminare ogni indice e vincolo, aggiornare la tabella, quindi aggiungere nuovamente tutti gli indici / vincoli.

La scrittura di una transazione SQL ha richiesto circa 3 minuti:

  • INIZIO;
  • indici / consegne rilasciati
  • tabella di aggiornamento
  • aggiungere nuovamente indici / vincoli
  • COMMETTERE;

L'esecuzione dello script ha richiesto 7 minuti.

La risposta accettata è decisamente migliore e più corretta ... e praticamente elimina la necessità di tempi di inattività. Nel mio caso, però, ci sarebbe voluto molto più lavoro da "sviluppatore" per utilizzare quella soluzione e avevamo una finestra di 30 minuti di inattività programmata che poteva essere realizzata. La nostra soluzione ha risolto il problema in 10.

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.