Hinzufügen von 'serial' zu einer vorhandenen Spalte in Postgres

91

Ich habe eine kleine Tabelle (~ 30 Zeilen) in meiner Postgres 9.0-Datenbank mit einem Ganzzahl-ID-Feld (dem Primärschlüssel), das derzeit eindeutige aufeinanderfolgende Ganzzahlen ab 1 enthält, die jedoch nicht mit dem Schlüsselwort 'serial' erstellt wurden.

Wie kann ich diese Tabelle so ändern, dass Einfügungen in diese Tabelle von nun an dazu führen, dass sich dieses Feld so verhält, als ob es mit dem Typ 'serial' erstellt worden wäre?

nicolaskruchten
quelle
5
Zu Ihrer Information, der SERIALPseudotyp ist jetzt ein Legacy -Typ , der durch die neue GENERATED … AS IDENTITYFunktion ersetzt wird, die in SQL: 2003 , in Postgres 10 und höher definiert ist. Siehe Erklärung .
Basil Bourque
Für die moderne Postgres-Version (> = 10) siehe diese Frage: stackoverflow.com/questions/2944499
a_horse_with_no_name

Antworten:

132

Sehen Sie sich die folgenden Befehle an (insbesondere den kommentierten Block).

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
quelle
Da Sie in Ihrem OP Primärschlüssel erwähnen, möchten Sie dies möglicherweise auch ALTER TABLE foo ADD PRIMARY KEY (a).
Skippy le Grand Gourou
SERIAL ist syntaktischer Zucker und wird nicht in den DB-Metadaten gespeichert, sodass der obige Code zu 100% äquivalent wäre.
DKroot
Wenn die Möglichkeit besteht, dass die Zieltabelle von einem anderen Benutzer erstellt wurde, müssen Sie dies ALTER TABLE foo OWNER TO current_user;zuerst tun .
DKroot
2
Sollten Sie nicht MAX(a)+1auf setval setzen? SELECT MAX(a)+1 FROM foo; SELECT setval('foo_a_seq', 6);
SunnyPro
48

Sie können auch START WITHeine Sequenz von einem bestimmten Punkt aus starten, obwohl setval dasselbe bewirkt wie in Eulers Antwort, z.

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
quelle
28

TL; DR

Hier ist eine Version, in der Sie keinen Menschen benötigen, um einen Wert zu lesen und selbst einzugeben.

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'); 

Eine andere Möglichkeit wäre, das Functionam Ende dieser Antwort freigegebene wiederverwendbare zu verwenden .


Eine nicht interaktive Lösung

Fügen Sie einfach die beiden anderen Antworten hinzu, für diejenigen von uns, die diese Sequencevon einem nicht interaktiven Skript erstellen lassen müssen, während sie beispielsweise eine Live-Datenbank patchen.

Das heißt, wenn Sie SELECTden Wert nicht manuell eingeben und selbst in eine nachfolgende CREATEAnweisung eingeben möchten.

Kurz gesagt, Sie können nicht tun:

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

... da die START [WITH]Klausel in CREATE SEQUENCEeinen Wert erwartet , keine Unterabfrage.

Hinweis: Als Faustregel gilt , dass für alle nicht-CRUD gilt ( dh : als alles andere INSERT, SELECT, UPDATE, DELETE) Aussagen in pgSQL AFAIK.

Tut es setval()jedoch! Somit ist folgendes absolut in Ordnung:

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

Wenn keine Daten vorhanden sind und Sie nichts darüber wissen (möchten), coalesce()legen Sie den Standardwert fest:

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

Das Einstellen des aktuellen Sequenzwerts 0ist jedoch umständlich, wenn nicht sogar unzulässig.
Die Verwendung der Drei-Parameter-Form von setvalwäre angemessener:

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

Durch Einstellen des optionalen dritten Parameters von setvalto falsewird verhindert, dass der nächste nextvaldie Sequenz vor dem Zurückgeben eines Werts vorverlegt, und somit:

Der nächste nextvalgibt genau den angegebenen Wert zurück, und die Sequenzverbesserung beginnt mit dem folgenden nextval.

- Aus diesem Eintrag in der Dokumentation

In einem nicht verwandten Hinweis können Sie auch die Spalte angeben, die die Sequencedirekt mit CREATEbesitzt. Sie müssen sie später nicht mehr ändern:

CREATE SEQUENCE foo_a_seq OWNED BY foo.a;

Zusammenfassend:

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'); 

Verwendung einer Function

Wenn Sie dies für mehrere Spalten planen, können Sie alternativ eine tatsächliche verwenden 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;

Verwenden Sie es so:

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
quelle
Tolle Antwort, aber denken Sie coalesce(max(a), 0))daran, dass dies die meiste Zeit nicht funktioniert, da die IDs normalerweise bei 1 beginnen. Richtiger wärecoalesce(max(a), 1))
Amiko
1
Danke @Amiko für den Kommentar! Die setvalFunktion setzt eigentlich nur den aktuell "zuletzt verwendeten Wert" für die Sequenz. Der nächste verfügbare Wert (der erste, der tatsächlich verwendet wird) ist ein weiterer! Wenn Sie setval(..., coalesce(max(a), 1))eine leere Spalte verwenden, wird sie so eingestellt, dass sie mit 2(dem nächsten verfügbaren Wert) beginnt , wie in der Dokumentation dargestellt .
ccjmne
1
@Amiko Sie haben Recht, wenn Sie sagen, dass mein Code ein Problem enthält: Das currvalsollte niemals sein 0, auch wenn es sich nicht im tatsächlichen Datensatz widerspiegelt. Die Verwendung der Drei-Parameter-Form von setvalwäre angemessener : setval(..., coalesce(max(a), 0) + 1, false). Antwort entsprechend aktualisiert!
ccjmne
1
Einverstanden, das habe ich total vermisst. Danke für die Antwort, die mir Zeit gespart hat.
Amiko