Dodanie nowej wartości do istniejącego typu ENUM

208

Mam kolumnę tabeli, która używa enumtypu. Chcę zaktualizować ten enumtyp, aby miał dodatkową możliwą wartość. Nie chcę usuwać żadnych istniejących wartości, wystarczy dodać nową wartość. Jaki jest najprostszy sposób to zrobić?

Ian
źródło

Odpowiedzi:

153

UWAGA: jeśli korzystasz z PostgreSQL 9.1 lub nowszego i nie masz nic przeciwko wprowadzaniu zmian poza transakcją, zapoznaj się z tą odpowiedzią, aby uzyskać prostsze podejście.


Miałem ten sam problem kilka dni temu i znalazłem ten post. Moja odpowiedź może być więc pomocna dla kogoś, kto szuka rozwiązania :)

Jeśli masz tylko jedną lub dwie kolumny wykorzystujące typ wyliczenia, który chcesz zmienić, możesz spróbować. Możesz także zmienić kolejność wartości w nowym typie.

-- 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 należy powtórzyć, jeśli jest więcej niż 1 kolumna.

taksofan
źródło
9
Warto wspomnieć, że można to wszystko zrobić w ramach jednej transakcji, więc jest to w większości bezpieczne w produkcyjnej bazie danych.
David Leppik,
52
To nigdy nie był dobry pomysł. Od wersji 9.1 możesz to wszystko zrobić ALTER TYPE. Ale nawet wcześniej ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type;był znacznie lepszy.
Erwin Brandstetter,
1
Należy pamiętać, że starsze wersje Postgres nie obsługują typów zmiany nazw. W szczególności wersja Postgres na Heroku (współużytkowana baza danych, sądzę, że używają PG 8.3) nie obsługuje tego.
Ortwin Gentz
13
Możesz zwinąć kroki 3, 4, 5 i 6 razem w jedną instrukcję:ALTER TABLE some_table ALTER COLUMN some_column TYPE some_enum_type USING some_column::text::some_enum_type;
glyphobet
3
Jeśli robisz to na stole na żywo, zablokuj go podczas procedury. Domyślny poziom izolacji transakcji w postgresql nie zapobiegnie wstawianiu nowych wierszy przez inne transakcje podczas tej transakcji, więc możesz pozostać z niepoprawnie wypełnionymi wierszami.
Sérgio Carvalho
421

PostgreSQL 9.1 wprowadza możliwość ALTER typów ENUM:

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';
Dariusz
źródło
1
co to jest „typ enum”? nazwa pola, nazwa pola table_field? albo coś innego? jak mam to uderzyć? Mam tabelę „ocen” i mam kolumnę „typ”. W zrzutie bazy danych dostaję to: CONSTRAINT grades_type_check CHECK ((((type) :: text = ANY ((ARRAY ['egzamin' :: odmienny znak, 'test'): : zmiana postaci, „dodatkowa” :: zmiana postaci, „w połowie” :: zmiana postaci, „końcowa” :: zmiana postaci]) :: tekst [])))
1
enum_type to tylko Twoja własna nazwa typu enum @mariotanenbaum. Jeśli twój enum jest „typem”, to powinieneś tego użyć.
Dariusz,
26
czy można go usunąć?
Ced
8
Dodanie do komentarza @DrewNoakes, jeśli używasz db-migrate (który działa w transakcji), możesz otrzymać błąd: BŁĄD: ALTER TYPE ... ADD nie może działać w bloku transakcji Rozwiązanie wspomniane jest tutaj (przez Hubbitus ): stackoverflow.com/a/41696273/1161370
Mahesh
1
nie można go usunąć, więc migracja dow jest niemożliwa, dlatego należy skorzystać z innych metod
Muhammad Umer
65

Możliwe rozwiązanie jest następujące; warunkiem wstępnym jest brak konfliktów w używanych wartościach wyliczeniowych. (np. usuwając wartość wyliczenia, upewnij się, że ta wartość nie jest już używana).

-- 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__;

Również w ten sposób kolejność kolumn nie zostanie zmieniona.

Steffen
źródło
1
+1 to jest droga do wersji wcześniejszej niż 9.1 i wciąż droga do usuwania lub modyfikacji elementów.
To zdecydowanie najlepsza odpowiedź dla mojego rozwiązania, które dodaje nowe wyliczenia do istniejącego typu wyliczenia, w którym zachowujemy wszystkie stare wyliczenia i dodajemy nowe. Dodatkowo nasz skrypt aktualizacji jest transakcyjny. Wspaniały post!
Darin Peterson
1
Genialna odpowiedź! Unika hacków, pg_enumktóre mogą uszkodzić rzeczy i jest transakcyjne, w przeciwieństwie do ALTER TYPE ... ADD.
NathanAldenSr
4
W przypadku, gdy kolumna ma wartość domyślną będzie pojawić się następujący błąd: default for column "my_column" cannot be cast automatically to type "my_enum". Będziesz musiał wykonać następujące czynności: 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

Jeśli wpadniesz w sytuację, w której powinieneś dodać enumwartości w transakcji, np. Wykonaj ją podczas migracji flyway na ALTER TYPEwyciągu, otrzymasz błąd ERROR: ALTER TYPE ... ADD cannot run inside a transaction block(patrz problem flyway nr 350 ), możesz dodać takie wartości pg_enumbezpośrednio jako obejście ( type_egais_unitsto nazwa celu 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 )
Hubbitus
źródło
9
Będzie to jednak wymagać przyznania uprawnień administratora, ponieważ zmienia to tabelę systemową.
asnelzin
22

Uzupełnienie @Dariusz 1

W przypadku Railsów 4.2.1 dostępna jest sekcja dokumentu:

== Migracje transakcyjne

Jeśli adapter bazy danych obsługuje transakcje DDL, wszystkie migracje zostaną automatycznie opakowane w transakcję. Istnieją jednak zapytania, których nie można wykonać w ramach transakcji, aw takich sytuacjach można wyłączyć automatyczne transakcje.

class ChangeEnum < ActiveRecord::Migration
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end
Kiko Castro
źródło
3
to! jeśli grasz enumami w nowoczesnych szynach, właśnie tego szukasz.
Eli Albert,
1
Świetnie, bardzo mi pomógł!
Dmytro Uhnichenko
10

Z dokumentacji Postgres 9.1 :

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

Przykład:

ALTER TYPE user_status ADD VALUE 'PROVISIONAL' AFTER 'NORMAL'
Peymankh
źródło
3
Również z dokumentacji: Porównania z dodaną wartością wyliczenia będą czasem wolniejsze niż porównania z udziałem tylko oryginalnych elementów typu wyliczenia. [... szczegółowy wycięty jako zbyt długi, by można go było było skomentować ...] Spowolnienie jest zwykle nieznaczne; ale jeśli ma to znaczenie, optymalną wydajność można odzyskać, usuwając i odtwarzając typ wyliczeniowy lub zrzucając i ponownie ładując bazę danych.
Aaron Zinman
8

Oświadczenie: Nie wypróbowałem tego rozwiązania, więc może nie działać ;-)

Powinieneś patrzeć pg_enum. Jeśli chcesz tylko zmienić etykietę istniejącego ENUM, wystarczy prosta AKTUALIZACJA.

Aby dodać nowe wartości ENUM:

  • Najpierw wstaw nową wartość do pg_enum. Jeśli nowa wartość musi być ostatnia, gotowe.
  • Jeśli nie (potrzebujesz nowej wartości ENUM pomiędzy istniejącymi), musisz zaktualizować każdą odrębną wartość w tabeli, przechodząc od najwyższego do najniższego ...
  • Następnie musisz tylko zmienić ich nazwy pg_enumw odwrotnej kolejności.

Ilustracja
Masz następujący zestaw etykiet:

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

i chcesz uzyskać:

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

następnie:

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

następnie:

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

I tak dalej...

benja
źródło
5

Nie mogę opublikować komentarza, więc powiem tylko, że aktualizacja pg_enum działa w Postgres 8.4. Aby skonfigurować nasze wyliczenia, dodałem nowe wartości do istniejących typów wyliczeń poprzez:

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

To trochę przerażające, ale ma sens, biorąc pod uwagę sposób, w jaki Postgres faktycznie przechowuje swoje dane.

Josiah
źródło
1
Świetna odpowiedź! Pomaga tylko dołączyć do nowego wyliczenia, ale oczywiście nie rozwiązuje sprawy, w której musisz ponownie zamówić.
Mahmoud Abdelkader
Oprócz wiodących znaków podkreślenia dla nazwy typu, są również rozróżniane wielkie i małe litery. Prawie straciłem rozum, próbując wybrać według nazwy typu z tabeli pg_type.
Mahesh
5

Aktualizacja pg_enum działa, podobnie jak sztuczka z kolumną pośrednią zaznaczona powyżej. Można także użyć UŻYWANIA magii, aby bezpośrednio zmienić typ kolumny:

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;

Dopóki nie masz żadnych funkcji, które wyraźnie wymagają lub zwracają ten wyliczenie, jesteś dobry. (pgsql będzie narzekać, gdy upuścisz ten typ, jeśli istnieje).

Zauważ też, że PG9.1 wprowadza instrukcję ALTER TYPE, która będzie działać na wyliczeniach:

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

Denis de Bernardy
źródło
Odpowiednią dokumentację dla PostgreSQL 9.1 można teraz znaleźć na 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;Ale w dużej mierze teraz nieistotne ...
Erwin Brandstetter,
Podobnie do tego, co powiedział Erwin, ... USING bar::typepracował dla mnie. Nie musiałem nawet określać ::text.
Daniel Werner
3

Najprościej: pozbądź się enumów. Nie można ich łatwo modyfikować i dlatego bardzo rzadko powinny być używane.


źródło
2
może wystarczy proste ograniczenie sprawdzania?
1
A jaki dokładnie jest problem przechowywania wartości jako ciągów?
5
@Grazer: w wersji 9.1 możesz dodawać wartości do enum ( depesz.com/index.php/2010/10/27/… ) - ale nadal nie możesz usunąć starych.
3
@WillSheppard - Myślę , że w zasadzie nigdy. Myślę, że niestandardowe typy oparte na tekście z ograniczeniami sprawdzającymi są o wiele lepsze w każdym przypadku.
3
@JackDouglas - jasne. Chciałbym wziąć domenę z czekiem na wyliczanie każdego dnia.
3

Nie można dodać komentarza do odpowiedniego miejsca, ale ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_typeustawienie domyślne w kolumnie nie powiodło się. Musiałem:

ALTER table ALTER COLUMN bar DROP DEFAULT;

i wtedy zadziałało.

Judy Morgan Loomis
źródło
2

na wszelki wypadek, jeśli używasz Railsów i masz kilka instrukcji, będziesz musiał wykonać je jeden po drugim, na przykład:

execute "ALTER TYPE XXX ADD VALUE IF NOT EXISTS 'YYY';"
execute "ALTER TYPE XXX ADD VALUE IF NOT EXISTS 'ZZZ';"
edymerchk
źródło
1

Oto bardziej ogólne, ale raczej szybko działające rozwiązanie, które oprócz zmiany samego typu aktualizuje wszystkie kolumny w bazie danych, używając go. Metodę można zastosować, nawet jeśli nowa wersja ENUM różni się o więcej niż jedną etykietę lub pomija niektóre z oryginalnych. Poniższy kod zastępuje my_schema.my_type AS ENUM ('a', 'b', 'c')się następującym 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();

Cały proces będzie przebiegał dość szybko, ponieważ jeśli kolejność etykiet będzie się utrzymywać, nie nastąpi faktyczna zmiana danych. Zastosowałem tę metodę na 5 tabelach, używając my_typei mając 50 000–70 000 wierszy w każdym, a cały proces trwał zaledwie 10 sekund.

Oczywiście funkcja zwróci wyjątek na wypadek, gdyby etykiety, których brakuje w nowej wersji ENUM, były używane gdzieś w danych, ale w takiej sytuacji i tak należy coś wcześniej zrobić.

Alexander Kachkaev
źródło
To jest naprawdę cenne. Problemem są jednak widoki wykorzystujące stary ENUM. Należy je usunąć i odtworzyć, co jest o wiele bardziej skomplikowane, biorąc pod uwagę inne widoki w zależności od upuszczonych. Nie mówiąc o typach kompozytowych ...
Ondřej Bouda
1

Dla tych, którzy szukają rozwiązania transakcji, wydaje się, że działają następujące.

Zamiast ENUM, A DOMAINstosuje się na typ TEXTz ograniczeniem sprawdzenie, czy wartość mieści się w podanym liście dozwolonych wartości (jak sugerują niektóre komentarze). Jedyny problem polega na tym, że żadne ograniczenie nie może zostać dodane (a więc i nie zmodyfikowane) do domeny, jeśli jest ono używane przez dowolny typ złożony (dokumenty mówią jedynie, że „powinno się to ostatecznie poprawić”). Takie ograniczenie można jednak obejść, stosując ograniczenie wywołujące funkcję, jak następuje.

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;

Wcześniej korzystałem z rozwiązania podobnego do przyjętej odpowiedzi, ale nie jest to dobre, gdy rozważane są widoki lub funkcje lub typy złożone (a zwłaszcza widoki z wykorzystaniem innych widoków przy użyciu zmodyfikowanych ENUM ...). Rozwiązanie zaproponowane w tej odpowiedzi wydaje się działać w każdych warunkach.

Jedyną wadą jest to, że nie są przeprowadzane żadne kontrole istniejących danych po usunięciu niektórych dozwolonych wartości (co może być dopuszczalne, szczególnie w przypadku tego pytania). ( ALTER DOMAIN test_domain VALIDATE CONSTRAINT val_checkNiestety wywołanie kończy się tym samym błędem, co dodanie nowego ograniczenia do domeny używanej przez typ złożony, niestety).

Należy pamiętać, że niewielkie modyfikacje, takie jak CHECK (value = ANY(get_allowed_values())), gdzie get_allowed_values()funkcja powrócił listę dozwolonych wartości, nie będzie działać - co jest dość dziwne, więc mam nadzieję, że rozwiązanie zaproponowane powyżej działa niezawodnie (to nie dla mnie, tak daleko ...). (faktycznie działa - to był mój błąd)

Ondřej Bouda
źródło
0

Jak omówiono powyżej, ALTERpolecenia nie można zapisać w transakcji. Sugerowanym sposobem jest wstawienie bezpośrednio do tabeli pg_enum za pomocą retrieving the typelem from pg_type tablei calculating the next enumsortorder number;

Poniżej znajduje się kod, którego używam. (Sprawdza, czy istnieje zduplikowana wartość przed wstawieniem (ograniczenie między enumtypid a nazwą enumlabel)

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

Zauważ, że nazwa twojego typu jest poprzedzona znakiem podkreślenia w tabeli pg_type. Ponadto w nazwie klauzuli typ musi być pisany małymi literami.

Teraz można to bezpiecznie zapisać w skrypcie migracji db.

Mahesh
źródło
-1

Nie wiem, czy mam inną opcję, ale możemy upuścić wartość, używając:

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;
Jardel
źródło
-2

Korzystając z Navicat, możesz przejść do typów (w widoku -> inne -> typy) - uzyskać widok projektu typu - i kliknąć przycisk „dodaj etykietę”.

jvv
źródło
1
Byłoby miło, ale w prawdziwym życiu nie jest przydatne:ERROR: cannot drop type foo because other objects depend on it HINT: Use DROP ... CASCADE to drop the dependent objects too.
Ortwin Gentz
Dziwne, działało dla mnie. (Nie jestem pewien, dlaczego używasz DROP, gdy TS chce tylko dodać wartość do pola wyliczeniowego)
jvv
1
Nie zrobiłem DROP specjalnie, ale poszedłem dokładnie po twojej procedurze. Zakładam, że Navicat wykonuje DROP za kulisami i kończy się niepowodzeniem. Używam Navicat 9.1.5 Lite.
Ortwin Gentz