Додавання нового значення до існуючого типу ENUM


208

У мене стовпчик таблиці, який використовує enumтип. Я хочу оновити цей enumтип, щоб мати додаткове можливе значення. Я не хочу видаляти жодні існуючі значення, просто додайте нове значення. Який найпростіший спосіб це зробити?

Відповіді:


153

ПРИМІТКА. Якщо ви використовуєте PostgreSQL 9.1 або новішої версії, і вам все в порядку із внесенням змін поза транзакцією, див. Цю відповідь для більш простого підходу.


У мене була така ж проблема кілька днів тому, і я знайшов цю посаду. Тож моя відповідь може бути корисною для того, хто шукає рішення :)

Якщо у вас є лише один або два стовпці, які використовують тип enum, який ви хочете змінити, ви можете спробувати це. Також ви можете змінити порядок значень у новому типі.

-- 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 слід повторити, якщо є більше 1 стовпця.


9
Варто зазначити, що це все можна зробити за одну транзакцію, тому це здебільшого безпечно робити у виробничій базі даних.
Девід Леппік

52
Це ніколи не було гарною ідеєю. З 9.1 ви можете все це зробити ALTER TYPE. Але навіть до цього ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type;був набагато вищим.
Ервін Брандстеттер

1
Майте на увазі, що старіші версії Postgres не підтримують перейменування типів. Зокрема, версія Postgres на Heroku (спільний db, я вважаю, що вони використовують PG 8.3) не підтримує його.
Ортвін Генц

13
Ви можете згорнути кроки 3, 4, 5 і 6 разом в одне твердження:ALTER TABLE some_table ALTER COLUMN some_column TYPE some_enum_type USING some_column::text::some_enum_type;
glyphobet

3
Якщо ви робите це на живому столі, блокуйте стіл під час процедури. Рівень ізоляції транзакцій за замовчуванням у postgresql не перешкоджатиме введенню нових рядків іншими транзакціями під час цієї транзакції, тому у вас можуть залишитися неправильно заселені рядки.
Серхіо Карвальо

422

PostgreSQL 9.1 представляє здатність до типів ALTER 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';

1
що таке "enum_type"? ім'я поля, ім’я_на_поля? чи щось інше? як мені це вдарити? У мене є таблиця "оцінок" і у мене стовпець "тип" І в db dump я отримую це: CONSTRAINT grade_type_check CHECK (((type) :: text = ANY ((ARRAY ['іспит ":: символи змінюються,' test ': : змінення символів, 'додатковий': :: зміна символів, 'середньотермінований' ::

1
enum_type - це лише власне ім’я типу enum @mariotanenbaum. Якщо ти твій перелік є "типом", то це те, що ти повинен використовувати.
Даріуш

26
чи можна її видалити?
Ced

8
Додаючи до коментаря @DrewNoakes, якщо ви використовуєте db-migrate (який працює в транзакції), ви можете отримати помилку: ПОМИЛКА: АЛТЕР ТИПУ ... ADD не може запуститися всередині блоку транзакцій. Тут вирішено рішення (від Hubbitus ): stackoverflow.com/a/41696273/1161370
Mahesh

1
ви не можете його видалити, щоб унеможливити міграцію, тому доведеться вдаватися до інших методів
Мухаммед Умер

65

Можливе рішення - таке; передумовою є відсутність конфліктів у використаних значеннях enum. (наприклад, видаляючи значення перерахунку, переконайтеся, що це значення більше не використовується)

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

Також таким чином порядок стовпців не буде змінено.


1
+1 - це шлях до попереднього 9.1 та ще й спосіб вилучення або зміни елементів.

Це, безумовно, найкраща відповідь на моє рішення, яке додає нових переліків до існуючого типу enum, де ми зберігаємо всі старі переліки та додаємо нові. Крім того, наш сценарій оновлення є транзакційним. Чудовий пост!
Дарин Петерсон

1
Блискуча відповідь! Уникайте хакерів навколо, pg_enumякі насправді можуть зламати речі та є транзакційними, на відміну від ALTER TYPE ... ADD.
NathanAldenSr

4
У разі , якщо ваш стовпець має значення за замовчуванням , ви отримаєте наступне повідомлення про помилку: default for column "my_column" cannot be cast automatically to type "my_enum". Вам доведеться зробити наступне: 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

Якщо ви потрапите в ситуацію, коли вам слід додавати enumзначення в транзакції, виконайте це під час міграції прольоту за ALTER TYPEзаявою, ви отримаєте помилку ERROR: ALTER TYPE ... ADD cannot run inside a transaction block(див. Випуск № 350 ), ви можете додати такі значення pg_enumбезпосередньо як обхідне рішення ( type_egais_unitsце ім'я цілі 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
Однак це потребує надання дозволу адміністратора, оскільки це зміни системної таблиці.
asnelzin

22

Доповнення @Dariusz 1

Для Rails 4.2.1 є цей розділ документа:

== Трансакційна міграція

Якщо адаптер бази даних підтримує транзакції DDL, всі міграції автоматично перетворюються на транзакцію. Існують запити, які ви не можете виконати всередині транзакції, і в цих ситуаціях ви можете вимкнути автоматичні транзакції.

class ChangeEnum < ActiveRecord::Migration
  disable_ddl_transaction!

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

3
це! якщо ви граєте з ентузіастами в сучасних рейках, це саме те, що ви шукаєте.
Елі Альберт

1
Чудово, мені дуже допомогли!
Дмитро Ухніченко

10

З Postgres 9.1 Документація :

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

Приклад:

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

3
Також з документації: Порівняння, що включають додану вартість перерахунків, іноді буде повільнішим, ніж порівняння за участю лише оригінальних членів типу enum. [.... детальний фрагмент як занадто довгий для коментаря stackoverflow ...] Уповільнення, як правило, незначне; але якщо це має значення, оптимальну продуктивність можна відновити, скинувши та відтворивши тип enum, або скидаючи та перезавантажуючи базу даних.
Аарон Зінман

8

Відмова: Я не пробував це рішення, тому воно може не працювати ;-)

Ви повинні дивитись pg_enum. Якщо ви хочете лише змінити мітку існуючого ENUM, це зробить простий UPDATE.

Щоб додати нові значення ENUM:

  • Спочатку вставте нове значення в pg_enum. Якщо нове значення має бути останнім, ви закінчите.
  • Якщо ні (вам потрібно встановити нове значення ENUM між існуючими), вам доведеться оновити кожне окреме значення у вашій таблиці, переходячи від верхнього до нижнього ...
  • Тоді вам доведеться просто перейменувати їх у pg_enumзворотному порядку.

Ілюстрація У
вас є такий набір міток:

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

і ви хочете отримати:

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

тоді:

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

тоді:

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

І так далі...



5

Я не можу публікувати коментар, тому просто скажу, що оновлення pg_enum працює у Postgres 8.4. Для того, як налаштовані наші переліки, я додав нові значення до існуючих типів enum за допомогою:

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

Це трохи страшно, але це має сенс, враховуючи те, як Postgres насправді зберігає свої дані.


1
Чудова відповідь! Допомагає лише для додавання нового переліку, але очевидно не вирішує випадок, коли вам доведеться повторно замовити.
Махмуд Абделькадер


Поряд із провідним підкресленням назви типу, вони також залежать від регістру. Я майже втратив розум, намагаючись вибрати за назвою типу з таблиці pg_type.
Махеш

5

Оновлення pg_enum працює, як і трюк про посередницький стовпчик, зазначений вище. Можна також використовувати USING magic, щоб безпосередньо змінити тип стовпця:

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;

Поки у вас немає функцій, які явно вимагають або повертають цю суму, ви добре. (pgsql поскаржиться, коли ви скасуєте тип, якщо такий є.)

Також зауважте, що PG9.1 представляє оператор ALTER TYPE, який буде працювати над перерахунками:

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


Відповідну документацію для PostgreSQL 9.1 тепер можна знайти за адресою 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;Але в значній мірі неважливо ...
Ервін Брандштеттер

Аналогічно тому, що сказав Ервін, ... USING bar::typeпрацював на мене. Мені навіть не потрібно було вказувати ::text.
Даніель Вернер

3

Найпростіше: позбутися від переліків. Вони не легко змінюються, і тому їх слід використовувати дуже рідко.


2
можливо, просте обмеження перевірки зробить?

1
І яка саме проблема зберігання значень як рядків?

5
@Grazer: у 9.1 ви можете додавати значення до enum ( depesz.com/index.php/2010/10/27/… ) - але ви все одно не можете видалити старі.

3
@WillSheppard - я думаю, що в принципі ніколи. Я думаю, що користувацькі типи, засновані на тексті з обмеженнями перевірки, набагато кращі в будь-якому випадку.

3
@JackDouglas - точно. Я б взяв домен із перевіркою на перерахунок у будь-який день.

3

Неможливо додати коментар у відповідне місце, але ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_typeза умовчанням стовпець не вдався. Мені довелося:

ALTER table ALTER COLUMN bar DROP DEFAULT;

і тоді це спрацювало.


3

на всякий випадок, якщо ви використовуєте Rails і у вас є кілька заяв, вам потрібно буде виконати по черзі, наприклад:

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

1

Ось більш загальне, але досить швидкодіюче рішення, яке крім зміни типу оновлює всі стовпці бази даних, що використовують його. Метод можна застосувати, навіть якщо нова версія ENUM відрізняється більш ніж однією міткою або відсутня частина оригіналу. Наведений нижче код Замінює my_schema.my_type AS ENUM ('a', 'b', 'c')з 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();

Весь процес запуститься досить швидко, адже якщо порядок міток зберігається, фактична зміна даних не відбудеться. Я застосував метод на 5 таблицях, використовуючи my_typeпо 50 000–70 000 рядків у кожній, і весь процес зайняв всього 10 секунд.

Звичайно, функція поверне виняток у тому випадку, якщо мітки, які відсутні у новій версії ENUM, використовуються десь у даних, але в такій ситуації щось потрібно зробити заздалегідь.


Це справді цінно. Однак проблема полягає у переглядах із використанням старого ENUM. Вони повинні бути скинуті та відтворені, що набагато складніше, враховуючи інші погляди залежно від викинутих. Не кажучи про складені типи ...
Ondřej Bouda

1

Для тих, хто шукає рішення між транзакціями, здається, працює наступне.

Замість ENUM, DOMAINповинен використовуватися на типі TEXTз обмеженням перевірки , що значення знаходиться в межах зазначеного списку допустимих значень (як це було запропоновано деякими коментарями). Єдина проблема полягає в тому, що жодне обмеження не може бути додане (і, отже, не змінено) домену, якщо воно використовується будь-яким складеним типом (документи просто говорять, що це "зрештою має бути вдосконалено"). Таке обмеження може бути подолане, однак, використовуючи обмеження, що викликає функцію, наступним чином.

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;

Раніше я використовував рішення, подібне до прийнятої відповіді, але це далеко не добре, коли будуть розглянуті види чи функції або складені типи (і особливо представлення, використовуючи інші погляди за допомогою модифікованих ENUMs ...). Рішення, запропоноване у цій відповіді, здається, працює в будь-яких умовах.

Єдиним недоліком є ​​те, що при видаленні деяких дозволених значень не проводиться перевірка існуючих даних (що може бути прийнятним, особливо для цього питання). (Виклик ALTER DOMAIN test_domain VALIDATE CONSTRAINT val_checkзакінчується тією ж помилкою, що і додавання нового обмеження до домену, використовуваного композиційним типом, на жаль.)

Зауважте, що незначна модифікація на зразок CHECK (value = ANY(get_allowed_values())), де get_allowed_values()функція повертає список дозволених значень, не буде працювати - що досить дивно, тому сподіваюся, що запропоноване вище рішення працює надійно (це для мене поки що ...). (це працює, насправді - це була моя помилка)


0

Як було сказано вище, ALTERкоманду не можна записати всередину транзакції. Запропонований спосіб - вставити в таблицю pg_enum безпосередньо, retrieving the typelem from pg_type tableі calculating the next enumsortorder number;

Далі йде код, який я використовую. (Перевіряє, чи існує подвійне значення перед вставкою (обмеження між enumtypid та 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'
    )

Зверніть увагу, що ваше ім'я типу є попередньо підкресленим у таблиці pg_type. Крім того, тип імені повинен бути всіма малими літерами в пункті, де.

Тепер це можна сміливо записати у ваш сценарій міграції db.


-1

Я не знаю, чи є інший варіант, але ми можемо скинути значення, використовуючи:

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

Під час використання Navicat ви можете перейти до типів (у режимі перегляду -> інші -> типи) - отримати дизайнерський вигляд типу - та натиснути кнопку «Додати ярлик».


1
Було б добре, але в реальному житті це не корисно:ERROR: cannot drop type foo because other objects depend on it HINT: Use DROP ... CASCADE to drop the dependent objects too.
Ortwin Gentz

Дивно, це працювало на мене. (Не впевнений, чому ви використовуєте DROP, коли TS лише хотів додати значення до поля enum)
jvv

1
Я спеціально не робив DROP, але пішов саме після вашої процедури. Я припускаю, що Navicat робить DROP за лаштунками і не вдається. Я використовую Navicat 9.1.5 Lite.
Ортвін Генц
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.