向现有的ENUM类型添加新值


208

我有一个使用enum类型的表列。我希望将其更新enum为其他可能的值。我不想删除任何现有值,只需添加新值即可。最简单的方法是什么?

Answers:


153

注意如果您使用的是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;

如果多于1列,则应重复3-6。


9
值得一提的是,这一切都可以在单个事务中完成,因此在生产数据库中进行操作最安全。
David Leppik

52
这绝不是一个好主意。从9.1开始,您可以使用ALTER TYPE。但即使在那之前,ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type;它仍然优越。
Erwin Brandstetter

1
请注意,较旧的Postgres版本不支持重命名类型。特别是Heroku上的Postgres版本(共享数据库,我相信他们使用PG 8.3)不支持它。
2011年

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 2013年

3
如果在活动表上执行此操作,请在此过程中锁定表。postgresql中的默认事务隔离级别不会阻止其他事务在此事务期间插入新行,因此可能会留下错误填充的行。
塞尔吉奥卡瓦略

421

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(((type):: text = ANY((ARRAY ['exam'::: character different,'test': :字符变化,“额外” ::字符变化,“中期” ::字符变化,“最终” ::字符变化]):: text [])))

1
enum_type只是您自己的枚举类型名称@mariotanenbaum。如果您的枚举是“类型”,则应使用它。
Dariusz 2014年

26
是否可以删除一个?
Ced

8
添加到@DrewNoakes的注释中,如果您正在使用db-migrate(在事务中运行),则可能会收到错误消息:错误:ALTER TYPE ... ADD无法在事务块内运行解决方案在此处提到(由Hubbitus提供) ):stackoverflow.com/a/41696273/1161370
Mahesh

1
您无法删除它,因此它使得陶氏迁移变得不可能,因此必须诉诸其他方法
Muhammad Umer

65

可能的解决方法如下:前提是,使用的枚举值不存在冲突。(例如,在删除枚举值时,请确保不再使用该值。)

-- 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之前的方法,也是删除或修改元素的方法。

到目前为止,这是我的解决方案的最佳答案,该解决方案将新的枚举添加到现有的枚举类型中,在此我们保留所有旧的枚举并添加新的枚举。此外,我们的更新脚本是事务性的。很棒的帖子!
Darin Peterson

1
辉煌的答案!避免hack pg_enum可能实际上破坏事物并且具有交易性,这与hack无关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声明的flyway迁移中执行它会出错ERROR: ALTER TYPE ... ADD cannot run inside a transaction block(请参见flyway问题#350),您可以将这些值pg_enum直接添加为解决方法(type_egais_units是target的名称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
太好了,对我有很大帮助!
Dmytro Uhnichenko,

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
同样来自文档:涉及增加的枚举值的比较有时会比仅涉及枚举类型的原始成员的比较慢。[....详细信息被删掉了太长时间,以至于stackoverflow评论...]减速通常并不重要;但是,如果重要的话,可以通过删除并重新创建枚举类型,或通过转储并重新加载数据库来获得最佳性能。
亚伦·辛曼

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中有效。对于我们的枚举的设置方式,我通过以下方式向现有枚举类型添加了新值:

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

这有点吓人,但是考虑到Postgres实际存储数据的方式,这是有道理的。


1
好答案!仅用于附加新的枚举,但显然不能解决必须重新排序的情况。
Mahmoud Abdelkader


除了类型名的下划线外,它们还区分大小写。我几乎迷失了试图从pg_type表中选择类型名的想法。
Mahesh

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
Daniel Werner

3

最简单:摆脱枚举。他们是不会轻易改变的,因此应该非常很少被使用。


2
也许一个简单的检查约束会做什么?

1
将值存储为字符串的确切问题是什么?

5
@Grazer:在9.1中,您可以将值添加到枚举(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;

然后它起作用了。


2

以防万一,如果您使用的是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的视图。必须删除并重新创建它们,考虑到其他依赖于删除视图的视图,这要复杂得多。不谈论复合类型...
的Ondrej布达

1

对于那些正在寻找交易中解决方案的人,以下方法似乎有用。

代替an ENUMDOMAIN应在类型上TEXT使用a并进行约束检查,以确保该值在指定的允许值列表之内(如某些注释所建议)。唯一的问题是,如果任何复合类型都使用了约束,则不能将约束添加到域(因此也不能修改)(文档只是说“应该最终加以改进”)。但是,可以使用调用函数的约束来解决这种限制,如下所示。

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不幸的是,对to的最终会产生与向复合类型所使用的域中添加新约束相同的错误。)

请注意,稍作修改,例如CHECK (value = ANY(get_allowed_values())),当get_allowed_values()函数返回的允许值的列表,是行不通的-这是很奇怪的,所以我希望提出的解决方案上面的工作可靠(它确实对我来说,到目前为止...)。(实际上有效-这是我的错误)


0

如上所述,ALTER命令不能写入事务内部。建议的方法是通过retrieving the typelem from pg_type table和直接插入pg_enum表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表中带有下划线。另外,在where子句中,Typname必须全部小写。

现在可以安全地将其写入数据库迁移脚本。


-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

很奇怪,对我有用。(不确定TS仅想向枚举字段添加值时为什么要使用DROP)
2011年

1
我没有专门做一个DROP,但是恰好在您完成手术之后。我认为Navicat会在后台执行DROP并失败。我正在使用Navicat 9.1.5 Lite。
2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.