Добавление нового значения в существующий тип ENUM

208

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

Ян
источник

Ответы:

154

ПРИМЕЧАНИЕ: если вы используете PostgreSQL 9.1 или более поздней версии и можете вносить изменения вне транзакции, см. Этот ответ для более простого подхода.


У меня была такая же проблема несколько дней назад и я нашел этот пост. Так что мой ответ может быть полезен для тех, кто ищет решение :)

Если у вас есть только один или два столбца, которые используют тип перечисления, который вы хотите изменить, вы можете попробовать это. Также вы можете изменить порядок значений в новом типе.

-- 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 столбца.

taksofan
источник
9
Стоит отметить, что все это можно сделать за одну транзакцию, поэтому в основном это безопасно в производственной базе данных.
Дэвид Леппик
52
Это никогда не было хорошей идеей. С 9.1 вы можете делать все это с ALTER TYPE. Но даже до этого ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type;был намного выше.
Эрвин Брандштеттер
1
Имейте в виду, что старые версии Postgres не поддерживают типы переименования. В частности, версия Postgres на Heroku (общая база данных, я думаю, они используют 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"? имя поля, имя поля таблицы? или что-то другое? как я должен ударить это? У меня есть таблица "оценки", и у меня есть столбец "тип" И в дамп базы данных я получаю это: CONSTRAINT grades_type_check CHECK (((тип) :: text = ANY ((ARRAY ['exam' :: изменение символов, 'тест': : символ меняется, 'extra' :: символ меняется, 'midterm' :: символ меняется, 'final' :: символ меняется]) :: text [])))
1
enum_type - это просто ваше собственное имя типа enum @mariotanenbaum. Если ваш enum относится к типу, то это то, что вы должны использовать.
Дариуш
26
возможно ли удалить один?
Ced
8
Если добавить комментарий @DrewNoakes, если вы используете db-migrate (который выполняется в транзакции), вы можете получить сообщение об ошибке: ОШИБКА: ALTER TYPE ... ADD не может быть запущен внутри блока транзакции. Решение упоминается здесь (Hubbitus ): stackoverflow.com/a/41696273/1161370
Махеш
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 и еще способ удаления или изменения элементов.
На данный момент это лучший ответ для моего решения, которое добавляет новые перечисления к существующему типу перечисления, где мы сохраняем все старые перечисления и добавляем новые. Кроме того, наш скрипт обновления является транзакционным. Отличный пост!
Дарин Петерсон
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 )
Hubbitus
источник
9
Однако это потребует предоставления прав администратора, поскольку это меняет системную таблицу.
Аснелзин
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'
Peymankh
источник
3
Также из документации: Сравнения, включающие добавленное значение перечисления, иногда будут медленнее, чем сравнения, включающие только оригинальные члены типа перечисления. [.... подробный отрывок слишком длинный для комментария по поводу переполнения стека ...] Замедление обычно незначительно; но если это имеет значение, оптимальная производительность может быть восстановлена ​​путем удаления и повторного создания типа enum или путем выгрузки и перезагрузки базы данных.
Аарон Зинман
8

Отказ от ответственности: я не пробовал это решение, поэтому оно может не работать ;-)

Вы должны смотреть на pg_enum. Если вы хотите изменить только метку существующего ENUM, простое ОБНОВЛЕНИЕ сделает это.

Чтобы добавить новые значения 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;

И так далее...

Benja
источник
5

Кажется, я не могу оставить комментарий, поэтому просто скажу, что обновление pg_enum работает в Postgres 8.4. Для настройки наших перечислений я добавил новые значения в существующие типы перечислений с помощью:

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

Это немного страшно, но имеет смысл, учитывая то, как Postgres на самом деле хранит свои данные.

Иосия
источник
1
Отличный ответ! Помогает только для добавления нового перечисления, но, очевидно, не решает вопрос о том, где вы должны изменить порядок.
Махмуд Абделькадер
Наряду с ведущим подчеркиванием для typename, они также чувствительны к регистру. Я почти сошел с ума, пытаясь выбрать по typename из таблицы pg_type.
Махеш
5

Работает обновление pg_enum, как и трюк с промежуточным столбцом, выделенный выше. Можно также использовать ИСПОЛЬЗОВАНИЕ магии, чтобы напрямую изменить тип столбца:

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
@ ДжекДуглас - конечно. Я бы взял домен с проверкой enum в любой день.
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';"
edymerchk
источник
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_type50 000–70 000 строк в каждой, и весь процесс занял всего 10 секунд.

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

Александр Качкаев
источник
Это действительно ценно. Однако проблема в представлениях, использующих старый ENUM. Они должны быть отброшены и воссозданы, что намного сложнее, если учитывать другие виды в зависимости от отброшенных. Не говоря уже о составных типах ...
Ондржей Буда
1

Для тех, кто ищет решение для транзакций, следующее работает.

Вместо ENUMa 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;

Ранее я использовал решение, аналогичное принятому ответу, но оно далеко не годится, если рассматривать представления или функции или составные типы (и особенно представления, использующие другие представления с использованием модифицированных ENUM ...). Решение, предложенное в этом ответе, похоже, работает в любых условиях.

Единственным недостатком является то, что никакие проверки не выполняются на существующих данных, когда некоторые разрешенные значения удалены (что может быть приемлемо, особенно для этого вопроса). (Вызов 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 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'
    )

Обратите внимание, что вашему имени типа предшествует знак подчеркивания в таблице pg_type. Кроме того, имя типа должно быть строчными в предложении where.

Теперь это можно безопасно записать в сценарий переноса БД.

Махеш
источник
-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 вы можете перейти к типам (в представлении -> другие -> типы) - получить представление дизайна типа - и нажать кнопку «добавить метку».

jvv
источник
1
Было бы неплохо, но в реальной жизни это бесполезно:ERROR: cannot drop type foo because other objects depend on it HINT: Use DROP ... CASCADE to drop the dependent objects too.
Ортвин Генц
Странно, у меня это сработало. (Не уверен, почему вы используете DROP, когда TS только хотел добавить значение в поле enum)
jvv
1
Я не делал DROP специально, но пошел именно после вашей процедуры. Я предполагаю, что Navicat делает DROP за кулисами и терпит неудачу. Я использую Navicat 9.1.5 Lite.
Ортвин Генц