Compactage d'une séquence dans PostgreSQL


9

J'ai une id serial PRIMARY KEYcolonne dans une table PostgreSQL. Beaucoup de ids sont manquants car j'ai supprimé la ligne correspondante.

Maintenant, je veux "compacter" la table en redémarrant la séquence et en réaffectant les ids de manière à ce que l' idordre d' origine soit conservé. C'est possible?

Exemple:

  • Maintenant:

 id | data  
----+-------
  1 | hello
  2 | world
  4 | foo
  5 | bar
  • Après:

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

J'ai essayé ce qui était suggéré dans une réponse StackOverflow , mais cela n'a pas fonctionné:

# 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.

Réponses:


9

Tout d'abord, des lacunes dans une séquence sont à prévoir. Demandez-vous si vous devez vraiment les supprimer. Votre vie devient plus simple si vous vivez avec. Pour obtenir des nombres sans écart, l'alternative (souvent meilleure) est d'utiliser un VIEWavec row_number(). Exemple dans cette réponse connexe:

Voici quelques recettes pour éliminer les lacunes.

1. Nouvelle table vierge

Évite les complications avec violations uniques et ballonnement de table et est rapide . Uniquement pour les cas simples où vous n'êtes pas lié par des références FK, des vues sur la table ou d'autres objets dépendants, ou par un accès simultané. Faites-le en une seule transaction pour éviter les accidents:

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)copie la structure incl. contraintes et valeurs par défaut de la table d'origine. Faites ensuite en sorte que la nouvelle colonne du tableau soit propriétaire de la séquence:

Et réinitialisez-le au nouveau maximum:

Cela présente l'avantage que la nouvelle table est sans ballonnement et en cluster id.

2. UPDATEen place

Cela produit beaucoup de lignes mortes et nécessite (auto-) VACUUMplus tard.

Si la serialcolonne est également la PRIMARY KEY(comme dans votre cas) ou a une UNIQUEcontrainte, vous devez éviter les violations uniques dans le processus. La valeur par défaut (la moins chère) pour les contraintes PK / UNIQUE doit être NOT DEFERRABLE, ce qui force une vérification après chaque ligne unique. Tous les détails sous cette question connexe sur SO:

Vous pouvez définir votre contrainte comme DEFERRABLE(ce qui la rend plus chère).
Ou vous pouvez supprimer la contrainte et l'ajouter à nouveau lorsque vous avez terminé:

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;

Cela n'est pas possible tant que vous avez desFOREIGN KEYcontraintes référençant la ou les colonnes car ( selon la documentation ):

Les colonnes référencées doivent être les colonnes d'une contrainte de clé unique ou primaire non reportable dans la table référencée.

Vous devez (verrouiller toutes les tables impliquées et) supprimer / recréer les contraintes FK et mettre à jour toutes les valeurs FK manuellement (voir option 3. ). Ou vous devez déplacer les valeurs avec une seconde UPDATEpour éviter les conflits. Par exemple, en supposant que vous n'avez pas de nombres négatifs:

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;

Inconvénients comme mentionné ci-dessus.

3. Tableau de Temp, TRUNCATE,INSERT

Une option de plus si vous avez beaucoup de RAM. Cela combine certains des avantages des deux premières façons. Presque aussi vite que l'option 1. et vous obtenez une nouvelle table vierge sans ballonnement mais gardez toutes les contraintes et dépendances en place comme dans l'option 2.
Cependant , selon la documentation:

TRUNCATE ne peut pas être utilisé sur une table qui a des références de clé étrangère provenant d'autres tables, sauf si toutes ces tables sont également tronquées dans la même commande. La vérification de la validité dans de tels cas nécessiterait des analyses de table, et le but n'est pas d'en faire une.

Accentuation mienne.

Vous pouvez supprimer temporairement les contraintes FK et utiliser des CTE de modification des données pour mettre à jour toutes les colonnes 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;

Connexes, avec plus de détails:


Si tous FOREIGN KEYSsont définis sur CASCADEImpossible, vous pouvez simplement parcourir les anciennes clés primaires et mettre à jour leurs valeurs sur place (de l'ancienne valeur à la nouvelle)? Il s'agit essentiellement de l'option 3 sans TRUNCATE tbl, remplaçant INSERTpar un UPDATE, et sans avoir besoin de mettre à jour les clés étrangères manuellement.
Gili

@Gili: Vous pourrait , mais ce genre de boucle est extrêmement coûteux. Étant donné que vous ne pouvez pas mettre à jour la table entière à la fois en raison de violations de clés uniques dans l'index, vous avez besoin d'une UPDATEcommande distincte pour chaque ligne. Voir explication en ② ou essayez de voir par vous-même.
Erwin Brandstetter

Je ne pense pas que la performance soit un problème dans mon cas. À mon avis, il existe deux types d'algorithmes: ceux qui "arrêtent le monde" et ceux qui s'exécutent tranquillement en arrière-plan sans avoir à arrêter le serveur. En supposant que le compactage ne se produit qu'une seule fois dans une lune bleue (par exemple lorsque vous approchez de la limite supérieure d'un type de données), il n'y a pas vraiment de limite supérieure sur le temps que cela devrait prendre. Tant que nous compactons des enregistrements plus rapidement que de nouveaux sont ajoutés, nous devrions être bien.
Gili
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.