Compattazione di una sequenza in PostgreSQL


9

Ho una id serial PRIMARY KEYcolonna in una tabella PostgreSQL. Mancano molti messaggi idperché ho eliminato la riga corrispondente.

Ora voglio "compattare" la tabella riavviando la sequenza e riassegnando le ids in modo tale da idpreservare l' ordine originale . È possibile?

Esempio:

  • Adesso:

 id | data  
----+-------
  1 | hello
  2 | world
  4 | foo
  5 | bar
  • Dopo:

 id | data  
----+-------
  1 | hello
  2 | world
  3 | foo
  4 | bar

Ho provato ciò che è stato suggerito in una risposta StackOverflow , ma non ha funzionato:

# alter sequence t_id_seq restart;
ALTER SEQUENCE
# update t set id=default;
ERROR:  duplicate key value violates unique constraint t_pkey
DETAIL:  Key (id)=(1) already exists.

Risposte:


9

Prima di tutto, sono attesi vuoti in una sequenza. Chiediti se hai davvero bisogno di rimuoverli. La tua vita diventa più semplice se vivi con essa. Per ottenere numeri senza gap, l'alternativa (spesso migliore) è usare un VIEWcon row_number(). Esempio in questa risposta correlata:

Ecco alcune ricette per rimuovere le lacune.

1. Tabella nuova e incontaminata

Evita le complicazioni con violazioni uniche e gonfiore del tavolo ed è veloce . Solo per casi semplici in cui non sei vincolato da riferimenti FK, viste sulla tabella o altri oggetti dipendenti o da accesso simultaneo. Fallo in una transazione per evitare incidenti:

BEGIN;
LOCK tbl;

CREATE TABLE tbl_new (LIKE tbl INCLUDING ALL);

INSERT INTO tbl_new -- no target list in this case
SELECT row_number() OVER (ORDER BY id), data  -- all columns in default order
FROM   tbl;

ALTER SEQUENCE tbl_id_seq OWNED BY tbl_new.id;  -- make new table own sequence

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME TO tbl;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

COMMIT;

CREATE TABLE tbl_new (LIKE tbl INCLUDING ALL)copia la struttura incl. vincoli e valori predefiniti dalla tabella originale. Quindi rendere la nuova colonna della tabella propria della sequenza:

E ripristinalo al nuovo massimo:

Questo porta il vantaggio che la nuova tabella è esente da gonfiori e raggruppata id.

2. UPDATEin atto

Questo produce molte file morte e richiede (auto-) in VACUUMseguito.

Se la serialcolonna è anche PRIMARY KEY(come nel tuo caso) o presenta un UNIQUEvincolo, devi evitare violazioni uniche nel processo. L'impostazione predefinita (più economica) per i vincoli PK / UNIQUE è NOT DEFERRABLE, che forza un controllo dopo ogni singola riga. Tutti i dettagli in questa domanda correlata su SO:

È possibile definire il vincolo come DEFERRABLE(che lo rende più costoso).
Oppure puoi eliminare il vincolo e aggiungerlo nuovamente al termine:

BEGIN;

LOCK tbl;

ALTER TABLE tbl DROP CONSTRAINT tbl_pkey;  -- remove PK

UPDATE tbl t  -- intermediate unique violations are ignored now
SET    id = t1.new_id
FROM  (SELECT id, row_number() OVER (ORDER BY id) AS new_id FROM tbl) t1
WHERE  t.id = t1.id;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

ALTER TABLE tbl ADD CONSTRAINT tbl_pkey PRIMARY KEY(id); -- add PK back

COMMIT;

Nessuno dei due è possibile mentre si hannoFOREIGN KEYvincoli che fanno riferimento alle colonne perché ( per documentazione ):

Le colonne di riferimento devono essere le colonne di un vincolo di chiave unica o primaria non differibile nella tabella di riferimento.

Dovresti (bloccare tutte le tabelle coinvolte e) eliminare / ricreare i vincoli FK e aggiornare tutti i valori FK manualmente (vedi opzione 3. ). Oppure devi spostare i valori di mezzo con un secondo UPDATEper evitare conflitti. Ad esempio, supponendo che non ci siano numeri negativi:

BEGIN;
LOCK tbl;

UPDATE tbl SET id = id * -1;  -- avoid conflicts

UPDATE tbl t
SET    id = t1.new_id
FROM  (SELECT id, row_number() OVER (ORDER BY id DESC) AS new_id FROM tbl) t1
WHERE  t.id = t1.id;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

COMMIT;

Svantaggi come menzionato sopra.

3. tabella temporanea TRUNCATE,INSERT

Un'altra opzione se hai molta RAM. Questo combina alcuni dei vantaggi dei primi due modi. Quasi veloce come l'opzione 1. e ottieni una nuova tabella incontaminata senza gonfiare, ma mantieni tutti i vincoli e le dipendenze in atto come nell'opzione 2.
Tuttavia , per documentazione:

TRUNCATE non può essere utilizzato su una tabella che ha riferimenti a chiave esterna da altre tabelle, a meno che anche tutte queste tabelle vengano troncate nello stesso comando. Il controllo della validità in questi casi richiederebbe scansioni di tabelle e l'intero punto non è quello di eseguirne una.

Enorme enfasi sulla mia.

È possibile eliminare temporaneamente i vincoli FK e utilizzare CTE che modificano i dati per aggiornare tutte le colonne FK:

SET temp_buffers = 500MB;   -- example value, see 1st link below

BEGIN;

CREATE TEMP TABLE tbl_tmp AS
SELECT row_number() OVER (ORDER BY id) AS new_id, *
FROM   tbl
ORDER  BY id;  -- order here to use index (if one exists)

-- drop FK constraints in other tables referencing this one
-- which takes out an exclusive lock on those tables

TRUNCATE tbl;

INSERT INTO tbl
SELECT new_id, data  -- list all columns in order
FROM tbl_tmp;        -- rely on established order in tbl_tmp
-- ORDER BY id;      -- only to be absolutely sure (not necessary)

--  example for table "fk_tbl" with FK column "fk_id"
UPDATE fk_tbl f
SET    fk_id = t.new_id  -- set to new ID
FROM   tbl_tmp t
WHERE  f.fk_id = t.id;   -- match on old ID

-- add FK constraints in other tables back

COMMIT;

Correlato, con maggiori dettagli:


Se tutti FOREIGN KEYSsono impostati su CASCADEnon potresti semplicemente passare in rassegna le vecchie chiavi primarie e aggiornare i loro valori sul posto (dal vecchio valore a quello nuovo)? In sostanza, questa è l'opzione 3 senza TRUNCATE tbl, sostituendo INSERTcon una UPDATEe senza la necessità di aggiornare manualmente le chiavi esterne.
Gili,

@Gili: È possibile , ma questo tipo di circuito è estremamente costoso. Poiché non è possibile aggiornare l'intera tabella in una sola volta a causa di violazioni di chiavi univoche nell'indice, è necessario un UPDATEcomando separato per ogni riga. Vedi spiegazione in ② o prova a vedere di persona.
Erwin Brandstetter,

Non penso che le prestazioni siano un problema nel mio caso. Per come la vedo io, ci sono due tipi di algoritmi: quelli che "fermano il mondo" e quelli che girano tranquillamente in background senza dover smontare il server. Supponendo che la compattazione avvenga solo una volta in una luna blu (ad es. Quando ci si avvicina al limite superiore di un tipo di dati), non esiste davvero un limite superiore per il tempo necessario. Finché stiamo compattando i record più velocemente di quelli nuovi, dovremmo andare bene.
Gili,
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.