Hinzufügen eines neuen Werts zu einem vorhandenen ENUM-Typ


208

Ich habe eine Tabellenspalte, die einen enumTyp verwendet. Ich möchte diesen enumTyp aktualisieren, um einen zusätzlichen möglichen Wert zu erhalten. Ich möchte keine vorhandenen Werte löschen, sondern nur den neuen Wert hinzufügen. Was ist der einfachste Weg, dies zu tun?

Antworten:


153

HINWEIS Wenn Sie PostgreSQL 9.1 oder höher verwenden und Änderungen außerhalb einer Transaktion vornehmen können, finden Sie in dieser Antwort einen einfacheren Ansatz.


Ich hatte vor ein paar Tagen das gleiche Problem und fand diesen Beitrag. Meine Antwort kann also für jemanden hilfreich sein, der nach einer Lösung sucht :)

Wenn Sie nur eine oder zwei Spalten haben, die den Aufzählungstyp verwenden, den Sie ändern möchten, können Sie dies versuchen. Sie können auch die Reihenfolge der Werte im neuen Typ ändern.

-- 1. rename the enum type you want to change
alter type some_enum_type rename to _some_enum_type;
-- 2. create new type
create type some_enum_type as enum ('old', 'values', 'and', 'new', 'ones');
-- 3. rename column(s) which uses our enum type
alter table some_table rename column some_column to _some_column;
-- 4. add new column of new type
alter table some_table add some_column some_enum_type not null default 'new';
-- 5. copy values to the new column
update some_table set some_column = _some_column::text::some_enum_type;
-- 6. remove old column and type
alter table some_table drop column _some_column;
drop type _some_enum_type;

3-6 sollte wiederholt werden, wenn mehr als eine Spalte vorhanden ist.


9
Es ist erwähnenswert, dass dies alles in einer einzigen Transaktion erledigt werden kann, daher ist es meistens sicher, dies in einer Produktionsdatenbank zu tun.
David Leppik

52
Das war nie eine gute Idee. Seit 9.1 können Sie alles damit machen ALTER TYPE. Aber schon vorher ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type;war weit überlegen.
Erwin Brandstetter

1
Beachten Sie, dass ältere Versionen von Postgres das Umbenennen von Typen nicht unterstützen. Insbesondere die Version von Postgres auf Heroku (Shared DB, ich glaube, sie verwenden PG 8.3) unterstützt dies nicht.
Ortwin Gentz

13
Sie können die Schritte 3, 4, 5 und 6 zu einer einzigen Anweisung zusammenfassen:ALTER TABLE some_table ALTER COLUMN some_column TYPE some_enum_type USING some_column::text::some_enum_type;
Glyphobet

3
Wenn Sie dies auf einer Live-Tabelle tun, sperren Sie die Tabelle während des Vorgangs. Die Standard-Transaktionsisolationsstufe in postgresql verhindert nicht, dass während dieser Transaktion neue Zeilen von anderen Transaktionen eingefügt werden, sodass möglicherweise falsch ausgefüllte Zeilen übrig bleiben.
Sérgio Carvalho

421

PostgreSQL 9.1 bietet die Möglichkeit, die Aufzählungstypen zu ändern:

ALTER TYPE enum_type ADD VALUE 'new_value'; -- appends to list
ALTER TYPE enum_type ADD VALUE 'new_value' BEFORE 'old_value';
ALTER TYPE enum_type ADD VALUE 'new_value' AFTER 'old_value';

1
Was ist der "enum_type"? Feldname, Tabellenfeldname? oder etwas anderes? Wie soll ich das treffen? Ich habe Tabelle "Noten" und ich habe Spalte "Typ" Und in DB Dump bekomme ich diese: CONSTRAINT Grades_Type_check CHECK (((Typ) :: Text = ANY ((ARRAY ['Prüfung' :: Zeichen variiert, 'Test': : Zeichen variieren, 'extra' :: Zeichen variieren, 'mittelfristig' :: Zeichen variieren, 'final' :: Zeichen variieren]) :: text [])))

1
enum_type ist nur ein eigener Aufzählungstypname @mariotanenbaum. Wenn Sie Ihre Aufzählung ein "Typ" ist, sollten Sie dies verwenden.
Dariusz

26
ist es möglich einen zu entfernen?
Ced

8
Wenn Sie den Kommentar von @DrewNoakes hinzufügen und db-migrate verwenden (das in der Transaktion ausgeführt wird), wird möglicherweise die folgende Fehlermeldung angezeigt: FEHLER: ALTER TYPE ... ADD kann nicht in einem Transaktionsblock ausgeführt werden Die Lösung wird hier erwähnt (von Hubbitus ): stackoverflow.com/a/41696273/1161370
Mahesh

1
Sie können es nicht entfernen, so dass eine Migration nach unten unmöglich ist. Sie müssen also auf andere Methoden zurückgreifen
Muhammad Umer,

65

Eine mögliche Lösung ist die folgende; Voraussetzung ist, dass es keine Konflikte in den verwendeten Enum-Werten gibt. (Stellen Sie z. B. beim Entfernen eines Aufzählungswerts sicher, dass dieser Wert nicht mehr verwendet wird.)

-- rename the old enum
alter type my_enum rename to my_enum__;
-- create the new enum
create type my_enum as enum ('value1', 'value2', 'value3');

-- alter all you enum columns
alter table my_table
  alter column my_column type my_enum using my_column::text::my_enum;

-- drop the old enum
drop type my_enum__;

Auch auf diese Weise wird die Spaltenreihenfolge nicht geändert.


1
+1 Dies ist der Weg vor 9.1 und dennoch der Weg zum Löschen oder Ändern von Elementen.

Dies ist bei weitem die beste Antwort für meine Lösung, bei der einem vorhandenen Aufzählungstyp neue Aufzählungen hinzugefügt werden, wobei alle alten Aufzählungen beibehalten und neue hinzugefügt werden. Zusätzlich ist unser Update-Skript transaktional. Guter Eintrag!
Darin Peterson

1
Geniale Antwort! Vermeidet Hacks pg_enum, die Dinge kaputt machen können und im Gegensatz zu Transaktionen ALTER TYPE ... ADD.
NathanAldenSr

4
Wenn Ihre Spalte einen Standardwert hat, wird folgende Fehlermeldung angezeigt : default for column "my_column" cannot be cast automatically to type "my_enum". Sie müssen Folgendes tun: ALTER TABLE "my_table" ALTER COLUMN "my_column" DROP DEFAULT, ALTER COLUMN "my_column" TYPE "my_type" USING ("my_column"::text::"my_type"), ALTER COLUMN "my_column" SET DEFAULT 'my_default_value';
n1ru4l

30

Wenn Sie in eine Situation geraten, in der Sie enumWerte in eine Transaktion einfügen sollten, z. B. wenn Sie diese in der Flyway-Migration ausführen, erhalten Sie ALTER TYPEeine Fehlermeldung ERROR: ALTER TYPE ... ADD cannot run inside a transaction block(siehe Flyway-Problem Nr. 350 ). Sie können solche Werte pg_enumdirekt als Problemumgehung hinzufügen ( type_egais_unitsist der Name des Ziels enum):

INSERT INTO pg_enum (enumtypid, enumlabel, enumsortorder)
    SELECT 'type_egais_units'::regtype::oid, 'NEW_ENUM_VALUE', ( SELECT MAX(enumsortorder) + 1 FROM pg_enum WHERE enumtypid = 'type_egais_units'::regtype )

9
Dies erfordert jedoch die Erteilung von Administratorrechten, da die Systemtabelle geändert wird.
Asnelzin

22

Ergänzung zu @Dariusz 1

Für Rails 4.2.1 gibt es diesen Dokumentabschnitt:

== Transaktionsmigrationen

Wenn der Datenbankadapter DDL-Transaktionen unterstützt, werden alle Migrationen automatisch in eine Transaktion eingeschlossen. Es gibt Abfragen, die Sie innerhalb einer Transaktion nicht ausführen können, und in diesen Situationen können Sie die automatischen Transaktionen deaktivieren.

class ChangeEnum < ActiveRecord::Migration
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end

3
Dies! Wenn Sie mit Aufzählungen in modernen Schienen spielen, ist dies genau das, wonach Sie suchen.
Eli Albert

1
Großartig, hat mir sehr geholfen!
Dmytro Uhnichenko

10

Aus Postgres 9.1 Dokumentation :

ALTER TYPE name ADD VALUE new_enum_value [ { BEFORE | AFTER } existing_enum_value ]

Beispiel:

ALTER TYPE user_status ADD VALUE 'PROVISIONAL' AFTER 'NORMAL'

3
Ebenfalls aus der Dokumentation: Vergleiche mit einem zusätzlichen Aufzählungswert sind manchmal langsamer als Vergleiche mit nur ursprünglichen Mitgliedern des Aufzählungstyps. [.... detailliert als zu lang für Stackoverflow-Kommentar abgeschnitten ...] Die Verlangsamung ist normalerweise unbedeutend; Wenn es jedoch darauf ankommt, kann die optimale Leistung durch Löschen und Neuerstellen des Aufzählungstyps oder durch Speichern und erneutes Laden der Datenbank wiederhergestellt werden.
Aaron Zinman

8

Haftungsausschluss: Ich habe diese Lösung nicht ausprobiert, daher funktioniert sie möglicherweise nicht ;-)

Sie sollten sich ansehen pg_enum. Wenn Sie nur die Bezeichnung einer vorhandenen ENUM ändern möchten, wird dies durch ein einfaches UPDATE erledigt.

So fügen Sie neue ENUM-Werte hinzu:

  • Fügen Sie zuerst den neuen Wert in ein pg_enum . Wenn der neue Wert der letzte sein muss, sind Sie fertig.
  • Wenn nicht (Sie müssen einen neuen ENUM-Wert zwischen den vorhandenen einfügen), müssen Sie jeden einzelnen Wert in Ihrer Tabelle aktualisieren, und zwar vom obersten zum niedrigsten ...
  • Dann müssen Sie sie nur noch pg_enumin umgekehrter Reihenfolge umbenennen .

Illustration
Sie haben die folgenden Beschriftungen:

ENUM ('enum1', 'enum2', 'enum3')

und Sie möchten erhalten:

ENUM ('enum1', 'enum1b', 'enum2', 'enum3')

dann:

INSERT INTO pg_enum (OID, 'newenum3');
UPDATE TABLE SET enumvalue TO 'newenum3' WHERE enumvalue='enum3';
UPDATE TABLE SET enumvalue TO 'enum3' WHERE enumvalue='enum2';

dann:

UPDATE TABLE pg_enum SET name='enum1b' WHERE name='enum2' AND enumtypid=OID;

Und so weiter...



5

Ich kann anscheinend keinen Kommentar veröffentlichen, daher sage ich nur, dass das Aktualisieren von pg_enum in Postgres 8.4 funktioniert. Für die Einrichtung unserer Aufzählungen habe ich vorhandenen Aufzählungstypen neue Werte hinzugefügt über:

INSERT INTO pg_enum (enumtypid, enumlabel)
  SELECT typelem, 'NEWENUM' FROM pg_type WHERE
    typname = '_ENUMNAME_WITH_LEADING_UNDERSCORE';

Es ist ein wenig beängstigend, aber angesichts der Art und Weise, wie Postgres seine Daten tatsächlich speichert, ist es sinnvoll.


1
Gute Antwort! Hilft nur beim Anhängen einer neuen Aufzählung, löst aber offensichtlich nicht den Fall, in dem Sie nachbestellen müssen.
Mahmoud Abdelkader


Neben dem führenden Unterstrich für den Typnamen wird auch zwischen Groß- und Kleinschreibung unterschieden. Ich habe fast den Verstand verloren, als ich versuchte, anhand des Typnamens aus der Tabelle pg_type auszuwählen.
Mahesh

5

Das Aktualisieren von pg_enum funktioniert ebenso wie der oben hervorgehobene Trick der Zwischenspalte. Sie können auch USING magic verwenden, um den Spaltentyp direkt zu ändern:

CREATE TYPE test AS enum('a', 'b');
CREATE TABLE foo (bar test);
INSERT INTO foo VALUES ('a'), ('b');

ALTER TABLE foo ALTER COLUMN bar TYPE varchar;

DROP TYPE test;
CREATE TYPE test as enum('a', 'b', 'c');

ALTER TABLE foo ALTER COLUMN bar TYPE test
USING CASE
WHEN bar = ANY (enum_range(null::test)::varchar[])
THEN bar::test
WHEN bar = ANY ('{convert, these, values}'::varchar[])
THEN 'c'::test
ELSE NULL
END;

Solange Sie keine Funktionen haben, die diese Aufzählung explizit erfordern oder zurückgeben, sind Sie gut. (pgsql wird sich beschweren, wenn Sie den Typ löschen, falls vorhanden.)

Beachten Sie außerdem, dass PG9.1 eine ALTER TYPE-Anweisung einführt, die für Aufzählungen funktioniert:

http://developer.postgresql.org/pgdocs/postgres/release-9-1-alpha.html


Die relevante Dokumentation für PostgreSQL 9.1 finden Sie jetzt unter postgresql.org/docs/9.1/static/sql-altertype.html
Wichert Akkerman

1
ALTER TABLE foo ALTER COLUMN bar TYPE test USING bar::text::new_type;Aber jetzt weitgehend irrelevant ...
Erwin Brandstetter

Ähnlich wie Erwin sagte, ... USING bar::typearbeitete für mich. Ich musste nicht einmal angeben ::text.
Daniel Werner

3

Am einfachsten: Aufzählungen loswerden. Sie sind nicht leicht zu modifizieren und sollten daher sehr selten verwendet werden.


2
Vielleicht reicht eine einfache Prüfbeschränkung?

1
Und was genau ist das Problem beim Speichern von Werten als Zeichenfolgen?

5
@Grazer: In 9.1 können Sie enum Werte hinzufügen ( depesz.com/index.php/2010/10/27/… ) - aber Sie können immer noch keine alten entfernen.

3
@ WillSheppard - Ich denke das im Grunde nie. Ich denke, dass benutzerdefinierte Typen, die auf Text mit Prüfbeschränkungen basieren, auf jeden Fall viel besser sind.

3
@ JackDouglas - sicher. Ich würde jeden Tag eine Domain mit Check-up-Aufzählung übernehmen.

3

Es kann kein Kommentar an der entsprechenden Stelle hinzugefügt werden, aber ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_typeein Standardwert für die Spalte ist fehlgeschlagen. Ich musste:

ALTER table ALTER COLUMN bar DROP DEFAULT;;

und dann hat es funktioniert.


2

Nur für den Fall, dass Sie Rails verwenden und mehrere Anweisungen haben, müssen Sie eine nach der anderen ausführen, wie zum Beispiel:

execute "ALTER TYPE XXX ADD VALUE IF NOT EXISTS 'YYY';"
execute "ALTER TYPE XXX ADD VALUE IF NOT EXISTS 'ZZZ';"

1

Hier ist eine allgemeinere, aber ziemlich schnell arbeitende Lösung, die neben der Änderung des Typs selbst alle Spalten in der Datenbank aktualisiert, die sie verwenden. Die Methode kann auch angewendet werden, wenn sich eine neue Version von ENUM um mehr als eine Bezeichnung unterscheidet oder einige der ursprünglichen fehlen. Der folgende Code ersetzt my_schema.my_type AS ENUM ('a', 'b', 'c')durch ENUM ('a', 'b', 'd', 'e'):

CREATE OR REPLACE FUNCTION tmp() RETURNS BOOLEAN AS
$BODY$

DECLARE
    item RECORD;

BEGIN

    -- 1. create new type in replacement to my_type
    CREATE TYPE my_schema.my_type_NEW
        AS ENUM ('a', 'b', 'd', 'e');

    -- 2. select all columns in the db that have type my_type
    FOR item IN
        SELECT table_schema, table_name, column_name, udt_schema, udt_name
            FROM information_schema.columns
            WHERE
                udt_schema   = 'my_schema'
            AND udt_name     = 'my_type'
    LOOP
        -- 3. Change the type of every column using my_type to my_type_NEW
        EXECUTE
            ' ALTER TABLE ' || item.table_schema || '.' || item.table_name
         || ' ALTER COLUMN ' || item.column_name
         || ' TYPE my_schema.my_type_NEW'
         || ' USING ' || item.column_name || '::text::my_schema.my_type_NEW;';
    END LOOP;

    -- 4. Delete an old version of the type
    DROP TYPE my_schema.my_type;

    -- 5. Remove _NEW suffix from the new type
    ALTER TYPE my_schema.my_type_NEW
        RENAME TO my_type;

    RETURN true;

END
$BODY$
LANGUAGE 'plpgsql';

SELECT * FROM tmp();
DROP FUNCTION tmp();

Der gesamte Prozess wird ziemlich schnell ausgeführt, da bei Beibehaltung der Reihenfolge der Beschriftungen keine tatsächliche Änderung der Daten erfolgt. Ich habe die Methode auf 5 Tabellen mit angewendetmy_type 50.000 bis 70.000 Zeilen , und der gesamte Vorgang dauerte nur 10 Sekunden.

Natürlich gibt die Funktion eine Ausnahme zurück, falls Beschriftungen, die in der neuen Version von ENUM fehlen, irgendwo in den Daten verwendet werden, aber in einer solchen Situation sollte sowieso vorher etwas getan werden.


Das ist wirklich wertvoll. Das Problem liegt jedoch bei Ansichten, die die alte ENUM verwenden. Sie müssen gelöscht und neu erstellt werden, was in Anbetracht anderer Ansichten abhängig von den abgelegten Ansichten weitaus komplizierter ist. Ich spreche nicht über zusammengesetzte Typen ...
Ondřej Bouda

1

Für diejenigen, die nach einer In-Transaction-Lösung suchen, scheint Folgendes zu funktionieren.

Anstelle von ENUMa DOMAINsoll a für einen Typ TEXTmit einer Einschränkung verwendet werden, die überprüft, ob der Wert innerhalb der angegebenen Liste der zulässigen Werte liegt (wie in einigen Kommentaren vorgeschlagen). Das einzige Problem besteht darin, dass einer Domäne keine Einschränkung hinzugefügt (und somit auch nicht geändert) werden kann, wenn sie von einem zusammengesetzten Typ verwendet wird (die Dokumentation sagt lediglich, dass dies "eventuell verbessert werden sollte"). Eine solche Einschränkung kann jedoch unter Verwendung einer Einschränkung, die eine Funktion aufruft, wie folgt umgangen werden.

START TRANSACTION;

CREATE FUNCTION test_is_allowed_label(lbl TEXT) RETURNS BOOL AS $function$
    SELECT lbl IN ('one', 'two', 'three');
$function$ LANGUAGE SQL IMMUTABLE;

CREATE DOMAIN test_domain AS TEXT CONSTRAINT val_check CHECK (test_is_allowed_label(value));

CREATE TYPE test_composite AS (num INT, word test_domain);

CREATE TABLE test_table (val test_composite);
INSERT INTO test_table (val) VALUES ((1, 'one')::test_composite), ((3, 'three')::test_composite);
-- INSERT INTO test_table (val) VALUES ((4, 'four')::test_composite); -- restricted by the CHECK constraint

CREATE VIEW test_view AS SELECT * FROM test_table; -- just to show that the views using the type work as expected

CREATE OR REPLACE FUNCTION test_is_allowed_label(lbl TEXT) RETURNS BOOL AS $function$
    SELECT lbl IN ('one', 'two', 'three', 'four');
$function$ LANGUAGE SQL IMMUTABLE;

INSERT INTO test_table (val) VALUES ((4, 'four')::test_composite); -- allowed by the new effective definition of the constraint

SELECT * FROM test_view;

CREATE OR REPLACE FUNCTION test_is_allowed_label(lbl TEXT) RETURNS BOOL AS $function$
    SELECT lbl IN ('one', 'two', 'three');
$function$ LANGUAGE SQL IMMUTABLE;

-- INSERT INTO test_table (val) VALUES ((4, 'four')::test_composite); -- restricted by the CHECK constraint, again

SELECT * FROM test_view; -- note the view lists the restricted value 'four' as no checks are made on existing data

DROP VIEW test_view;
DROP TABLE test_table;
DROP TYPE test_composite;
DROP DOMAIN test_domain;
DROP FUNCTION test_is_allowed_label(TEXT);

COMMIT;

Früher habe ich eine Lösung verwendet, die der akzeptierten Antwort ähnelt, aber es ist alles andere als gut, wenn Ansichten oder Funktionen oder zusammengesetzte Typen (und insbesondere Ansichten, die andere Ansichten mit den geänderten ENUMs verwenden ...) berücksichtigt werden. Die in dieser Antwort vorgeschlagene Lösung scheint unter allen Bedingungen zu funktionieren.

Der einzige Nachteil ist, dass keine Überprüfungen vorhandener Daten durchgeführt werden, wenn einige zulässige Werte entfernt werden (was insbesondere für diese Frage akzeptabel sein kann). (Ein Aufruf von ALTER DOMAIN test_domain VALIDATE CONSTRAINT val_checkendet am Ende mit demselben Fehler wie das Hinzufügen einer neuen Einschränkung zu der Domäne, die leider von einem zusammengesetzten Typ verwendet wird.)

Beachten Sie, dass eine geringfügige Änderung, z. B. CHECK (value = ANY(get_allowed_values()))wenn die get_allowed_values()Funktion die Liste der zulässigen Werte zurückgibt, nicht funktioniert - was ziemlich seltsam ist. Ich hoffe, dass die oben vorgeschlagene Lösung zuverlässig funktioniert (für mich bisher ...). (es funktioniert tatsächlich - es war mein Fehler)


0

Wie oben erläutert, ALTERkann der Befehl nicht in eine Transaktion geschrieben werden. Der vorgeschlagene Weg besteht darin, direkt, durch retrieving the typelem from pg_type tableund in die Tabelle pg_enum einzufügencalculating the next enumsortorder number .

Es folgt der Code, den ich verwende. (Überprüft vor dem Einfügen, ob ein doppelter Wert vorhanden ist (Einschränkung zwischen enumtypid und enumlabel name)

INSERT INTO pg_enum (enumtypid, enumlabel, enumsortorder)
    SELECT typelem,
    'NEW_ENUM_VALUE',
    (SELECT MAX(enumsortorder) + 1 
        FROM pg_enum e
        JOIN pg_type p
        ON p.typelem = e.enumtypid
        WHERE p.typname = '_mytypename'
    )
    FROM pg_type p
    WHERE p.typname = '_mytypename'
    AND NOT EXISTS (
        SELECT * FROM 
        pg_enum e
        JOIN pg_type p
        ON p.typelem = e.enumtypid
        WHERE e.enumlabel = 'NEW_ENUM_VALUE'
        AND p.typname = '_mytypename'
    )

Beachten Sie, dass Ihrem Typnamen in der Tabelle pg_type ein Unterstrich vorangestellt ist. Außerdem muss der Typname in der where-Klausel nur in Kleinbuchstaben angegeben werden.

Jetzt kann dies sicher in Ihr DB-Migrationsskript geschrieben werden.


-1

Ich weiß nicht, ob ich eine andere Option habe, aber wir können den Wert löschen mit:

select oid from pg_type where typname = 'fase';'
select * from pg_enum where enumtypid = 24773;'
select * from pg_enum where enumtypid = 24773 and enumsortorder = 6;
delete from pg_enum where enumtypid = 24773 and enumsortorder = 6;

-2

Wenn Sie Navicat verwenden, können Sie zu Typen gehen (unter Ansicht -> Andere -> Typen) - die Entwurfsansicht des Typs abrufen - und auf die Schaltfläche "Beschriftung hinzufügen" klicken.


1
Wäre schön, aber im wirklichen Leben ist es nicht nützlich:ERROR: cannot drop type foo because other objects depend on it HINT: Use DROP ... CASCADE to drop the dependent objects too.
Ortwin Gentz

Seltsam, es hat bei mir funktioniert. (Nicht sicher, warum Sie DROP verwenden, wenn TS nur einen Wert zum Aufzählungsfeld hinzufügen wollte)
jvv

1
Ich habe keinen DROP speziell gemacht, sondern bin genau nach Ihrem Eingriff gegangen. Ich gehe davon aus, dass Navicat den DROP hinter den Kulissen ausführt und fehlschlägt. Ich verwende Navicat 9.1.5 Lite.
Ortwin Gentz
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.