Ajout de 'serial' à la colonne existante dans Postgres

91

J'ai une petite table (~ 30 lignes) dans ma base de données Postgres 9.0 avec un champ d'ID entier (la clé primaire) qui contient actuellement des entiers séquentiels uniques commençant à 1, mais qui n'a pas été créé à l'aide du mot-clé 'serial'.

Comment puis-je modifier cette table de sorte qu'à partir de maintenant, les insertions dans cette table provoquent le comportement de ce champ comme s'il avait été créé avec 'serial' comme type?

nicolaskruchten
la source
5
Pour info, le SERIALpseudo-type est désormais hérité , supplanté par la nouvelle GENERATED … AS IDENTITYfonctionnalité définie dans SQL: 2003 , dans Postgres 10 et versions ultérieures. Voir l' explication .
Basil Bourque
Pour la version moderne de Postgres (> = 10) voir cette question: stackoverflow.com/questions/2944499
a_horse_with_no_name

Réponses:

132

Regardez les commandes suivantes (en particulier le bloc commenté).

DROP TABLE foo;
DROP TABLE bar;

CREATE TABLE foo (a int, b text);
CREATE TABLE bar (a serial, b text);

INSERT INTO foo (a, b) SELECT i, 'foo ' || i::text FROM generate_series(1, 5) i;
INSERT INTO bar (b) SELECT 'bar ' || i::text FROM generate_series(1, 5) i;

-- blocks of commands to turn foo into bar
CREATE SEQUENCE foo_a_seq;
ALTER TABLE foo ALTER COLUMN a SET DEFAULT nextval('foo_a_seq');
ALTER TABLE foo ALTER COLUMN a SET NOT NULL;
ALTER SEQUENCE foo_a_seq OWNED BY foo.a;    -- 8.2 or later

SELECT MAX(a) FROM foo;
SELECT setval('foo_a_seq', 5);  -- replace 5 by SELECT MAX result

INSERT INTO foo (b) VALUES('teste');
INSERT INTO bar (b) VALUES('teste');

SELECT * FROM foo;
SELECT * FROM bar;
Euler Taveira
la source
Puisque vous mentionnez des clés primaires dans votre OP, vous pouvez également le faire ALTER TABLE foo ADD PRIMARY KEY (a).
Skippy le Grand Gourou
SERIAL est du sucre syntaxique et n'est pas stocké dans les métadonnées de la base de données, le code ci-dessus serait donc équivalent à 100%.
DKroot
S'il y a une chance que la table cible ait été créée par un autre utilisateur, vous devez d' ALTER TABLE foo OWNER TO current_user;abord le faire .
DKroot
2
Ne devriez-vous pas vous installer MAX(a)+1à setval? SELECT MAX(a)+1 FROM foo; SELECT setval('foo_a_seq', 6);
SunnyPro
48

Vous pouvez également utiliser START WITHpour démarrer une séquence à partir d'un point particulier, bien que setval accomplisse la même chose, comme dans la réponse d'Euler, par exemple,

SELECT MAX(a) + 1 FROM foo;
CREATE SEQUENCE foo_a_seq START WITH 12345; -- replace 12345 with max above
ALTER TABLE foo ALTER COLUMN a SET DEFAULT nextval('foo_a_seq');
John Powell
la source
28

TL; DR

Voici une version où vous n'avez pas besoin d'un humain pour lire une valeur et la saisir lui-même.

CREATE SEQUENCE foo_a_seq OWNED BY foo.a;
SELECT setval('foo_a_seq', coalesce(max(a), 0) + 1, false) FROM foo;
ALTER TABLE foo ALTER COLUMN a SET DEFAULT nextval('foo_a_seq'); 

Une autre option serait d'utiliser le Functionpartage réutilisable à la fin de cette réponse.


Une solution non interactive

Il suffit d'ajouter aux deux autres réponses, pour ceux d'entre nous qui ont besoin de Sequencecréer ces s par un script non interactif , tout en corrigeant une base de données en direct par exemple.

Autrement dit, lorsque vous ne voulez pas SELECTla valeur manuellement et tapez-la vous-même dans une CREATEinstruction ultérieure .

Bref, vous ne pouvez pas faire:

CREATE SEQUENCE foo_a_seq
    START WITH ( SELECT max(a) + 1 FROM foo );

... puisque la START [WITH]clause in CREATE SEQUENCEattend une valeur , pas une sous-requête.

Note: En règle générale, applicable à tous les non-CRUD ( c. -à- : autre chose que INSERT, SELECT, UPDATE, DELETE) les déclarations pgSQL AFAIK.

Mais setval()oui! Ainsi, ce qui suit est absolument parfait:

SELECT setval('foo_a_seq', max(a)) FROM foo;

S'il n'y a pas de données et que vous ne (voulez) pas le savoir, utilisez coalesce()pour définir la valeur par défaut:

SELECT setval('foo_a_seq', coalesce(max(a), 0)) FROM foo;
--                         ^      ^         ^
--                       defaults to:       0

Cependant, avoir la valeur de séquence actuelle définie sur 0est maladroit, voire illégal.
L'utilisation de la forme à trois paramètres de setvalserait plus appropriée:

--                                             vvv
SELECT setval('foo_a_seq', coalesce(max(a), 0) + 1, false) FROM foo;
--                                                  ^   ^
--                                                is_called

La définition du troisième paramètre optionnel de setvalto falseempêchera le suivant nextvald'avancer la séquence avant de renvoyer une valeur, et donc:

le suivant nextvalrenverra exactement la valeur spécifiée et l'avancement de la séquence commence par ce qui suit nextval.

- à partir de cette entrée dans la documentation

Sur une note non liée, vous pouvez également spécifier la colonne possédant Sequencedirectement avec CREATE, vous n'avez pas à la modifier plus tard:

CREATE SEQUENCE foo_a_seq OWNED BY foo.a;

En résumé:

CREATE SEQUENCE foo_a_seq OWNED BY foo.a;
SELECT setval('foo_a_seq', coalesce(max(a), 0) + 1, false) FROM foo;
ALTER TABLE foo ALTER COLUMN a SET DEFAULT nextval('foo_a_seq'); 

Utilisant un Function

Sinon, si vous prévoyez de le faire pour plusieurs colonnes, vous pouvez opter pour l'utilisation d'un fichier réel Function.

CREATE OR REPLACE FUNCTION make_into_serial(table_name TEXT, column_name TEXT) RETURNS INTEGER AS $$
DECLARE
    start_with INTEGER;
    sequence_name TEXT;
BEGIN
    sequence_name := table_name || '_' || column_name || '_seq';
    EXECUTE 'SELECT coalesce(max(' || column_name || '), 0) + 1 FROM ' || table_name
            INTO start_with;
    EXECUTE 'CREATE SEQUENCE ' || sequence_name ||
            ' START WITH ' || start_with ||
            ' OWNED BY ' || table_name || '.' || column_name;
    EXECUTE 'ALTER TABLE ' || table_name || ' ALTER COLUMN ' || column_name ||
            ' SET DEFAULT nextVal(''' || sequence_name || ''')';
    RETURN start_with;
END;
$$ LANGUAGE plpgsql VOLATILE;

Utilisez-le comme ceci:

INSERT INTO foo (data) VALUES ('asdf');
-- ERROR: null value in column "a" violates not-null constraint

SELECT make_into_serial('foo', 'a');
INSERT INTO foo (data) VALUES ('asdf');
-- OK: 1 row(s) affected
ccjmne
la source
Excellente réponse, mais gardez à l'esprit que coalesce(max(a), 0))cela ne fonctionnera pas la plupart du temps, car les identifiants commencent généralement à partir de 1. Une manière plus correcte seraitcoalesce(max(a), 1))
Amiko
1
Merci @Amiko pour le commentaire! La setvalfonction définit en fait uniquement la "dernière valeur utilisée" actuelle pour la séquence. La prochaine valeur disponible (la première à être réellement utilisée) sera une de plus! Utiliser setval(..., coalesce(max(a), 1))sur une colonne vide la mettrait à "démarrer" avec 2(la prochaine valeur disponible), comme illustré dans la documentation .
ccjmne
1
@Amiko Vous avez raison de dire qu'il y a un problème dans mon code, cependant: cela currvalne devrait jamais être 0, même si cela ne serait pas reflété dans l'ensemble de données réel. En utilisant la forme à trois paramètres de setvalserait plus approprié: setval(..., coalesce(max(a), 0) + 1, false). Réponse mise à jour en conséquence!
ccjmne
1
D'accord, j'ai totalement raté ça. Merci pour la réponse qui m'a fait gagner du temps.
Amiko