【问题标题】:Adding a new value to an existing ENUM Type向现有 ENUM 类型添加新值
【发布时间】:2010-12-18 19:07:57
【问题描述】:

我有一个使用enum 类型的表格列。我希望更新 enum 类型以获得额外的可能值。我不想删除任何现有值,只需添加新值。最简单的方法是什么?

【问题讨论】:

    标签: database postgresql enums


    【解决方案1】:

    PostgreSQL 9.1 引入了ALTER 枚举类型的能力:

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

    【讨论】:

    • 什么是“enum_type”?字段名称,table_field 名称?或者是其他东西?我该怎么打呢?我有表“成绩”,我有列“类型”,在 db dump 中我得到这个: :字符变化, '额外'::字符变化, '中期'::字符变化, 'final'::字符变化])::text[])))
    • enum_type 只是您自己的枚举类型名称@mariotanenbaum。如果您的枚举是“类型”,那么这就是您应该使用的。
    • 可以去掉一个吗?
    • 添加到@DrewNoakes 的评论中,如果您使用 db-migrate(在事务中运行),那么您可能会收到错误:错误:ALTER TYPE ... ADD 无法在事务块内运行此处提到了解决方案(由 Hubbitus 提供):stackoverflow.com/a/41696273/1161370
    • 你不能删除它,所以它使dow迁移不可能,所以不得不求助于其他方法
    【解决方案2】:

    注意如果您使用的是 PostgreSQL 9.1 或更高版本,并且可以在事务之外进行更改,请参阅this answer 了解更简单的方法。


    几天前我遇到了同样的问题,并找到了这篇文章。所以我的回答可能对正在寻找解决方案的人有所帮助:)

    如果你只有一两列使用你想改变的枚举类型,你可以试试这个。您还可以更改新类型中值的顺序。

    -- 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.1 开始,您可以使用 ALTER TYPE 完成所有操作。但即使在此之前,ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type; 也远远优于。
    • 请注意旧版本的 Postgres 不支持重命名类型。特别是 Heroku 上的 Postgres 版本(共享数据库,我相信他们使用 PG 8.3)不支持它。
    • 您可以将步骤 3、4、5 和 6 合并为一个语句:ALTER TABLE some_table ALTER COLUMN some_column TYPE some_enum_type USING some_column::text::some_enum_type;
    • 如果在活动表上执行此操作,请在此过程中锁定该表。 postgresql 中的默认事务隔离级别不会阻止其他事务在此事务期间插入新行,因此您可能会留下错误填充的行。
    【解决方案3】:

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

    -- 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 这是 9.1 之前的方式,仍然是删除或修改元素的方式。
    • 这是迄今为止我的解决方案的最佳答案,它将新枚举添加到现有枚举类型,我们保留所有旧枚举并添加新枚举。此外,我们的更新脚本是事务性的。很棒的帖子!
    • 绝妙的答案!避免 pg_enum 周围的黑客攻击,这些黑客攻击实际上可以破坏事物并且是事务性的,这与 ALTER TYPE ... ADD 不同。
    • 如果您的列有默认值,您将收到以下错误: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';
    【解决方案4】:

    如果您遇到应该在事务中添加 enum 值的情况,例如在 ALTER TYPE 语句的 flyway 迁移中执行它,您将收到错误 ERROR: ALTER TYPE ... ADD cannot run inside a transaction block(请参阅 flyway issue #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 )
    

    【讨论】:

    • 但是,这需要授予管理员权限,因为它会更改系统表。
    • 或者您可以在单独的 flyway 迁移脚本中添加新值
    • Flyway 为其迁移管理事务。你说的是什么单独的脚本?
    【解决方案5】:

    如果您使用的是 Postgres 12(或更高版本),您可以在事务 (documentation) 内运行 ALTER TYPE ... ADD VALUE

    如果 ALTER TYPE ... ADD VALUE(向枚举添加新值的形式 type) 在事务块内执行,新值不能 在事务提交之前使用。

    因此迁移过程中不需要任何 hack。

    UPD:这是一个示例(感谢 Nick)

    ALTER TYPE enum_type ADD VALUE 'new_value';

    【讨论】:

    • 是的,例如:ALTER TYPE enum_type ADD VALUE 'new_value'; 谢谢!
    【解决方案6】:

    补充@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
    

    【讨论】:

    • 这个!如果您在现代 Rails 中使用枚举,这正是您正在寻找的。​​span>
    • 太好了,帮了我很多忙!
    【解决方案7】:

    来自 Postgres 9.1 Documentation:

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

    例子:

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

    【讨论】:

    • 另外来自文档:涉及添加枚举值的比较有时会比仅涉及枚举类型的原始成员的比较慢。 [....对于stackoverflow评论的详细剪辑太长了...]减速通常是微不足道的;但如果重要的话,可以通过删除和重新创建枚举类型或转储和重新加载数据库来恢复最佳性能。
    【解决方案8】:

    以防万一,如果您使用的是 Rails,并且您有多个语句,则需要一一执行,例如:

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

    【讨论】:

    • IF NOT EXISTS 位在我的工作中非常宝贵。谢谢你。
    【解决方案9】:

    免责声明:我没有尝试过这个解决方案,所以它可能不起作用;-)

    您应该查看pg_enum。如果您只想更改现有 ENUM 的标签,只需简单的 UPDATE 即可。

    要添加新的 ENUM 值:

    • 首先将新值插入pg_enum。如果新值必须是最后一个,那么您就完成了。
    • 如果不是(您需要在现有 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;
    

    等等……

    【解决方案10】:

    我似乎无法发表评论,所以我只想说更新 pg_enum 在 Postgres 8.4 中有效。对于我们的枚举设置方式,我通过以下方式向现有枚举类型添加了新值:

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

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

    【讨论】:

    【解决方案11】:

    更新 pg_enum 有效,就像上面突出显示的中间列技巧一样。也可以使用 USING 魔法直接改变列的类型:

    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找到
    • ALTER TABLE foo ALTER COLUMN bar TYPE test USING bar::text::new_type; 但现在基本上无关紧要了......
    • 与 Erwin 所说的类似,... USING bar::type 为我工作。我什至不必指定::text
    【解决方案12】:

    无法在适当的位置添加评论,但 ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_type 与列的默认值失败。我不得不:

    ALTER table ALTER COLUMN bar DROP DEFAULT;

    然后它起作用了。

    【讨论】:

      【解决方案13】:

      这是一个更通用但工作速度相当快的解决方案,除了更改类型本身之外,它还会更新使用它的数据库中的所有列。即使新版本的 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 的视图。它们必须被删除并重新创建,考虑到其他视图取决于删除的视图,这要复杂得多。不是说复合类型...
      【解决方案14】:

      对于那些正在寻找交易解决方案的人来说,以下似乎可行。

      应该在类型TEXT 上使用DOMAIN 而不是ENUM,并带有一个约束检查该值是否在指定的允许值列表内(如某些cmets 所建议的那样)。唯一的问题是,如果域被任何复合类型使用,则不能向域添加(因此也不能修改)约束(文档只是说“应该最终改进”)。但是,可以使用调用函数的约束来解决这种限制,如下所示。

      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() 函数返回允许值的列表,将无法正常工作 - 这很奇怪,所以我希望上面提出的解决方案能够可靠地工作(它确实对我来说,到目前为止......)。(它确实有效 - 这是我的错误)

      【讨论】:

        【解决方案15】:

        如上所述,ALTER 命令不能写入事务中。建议的方法是通过retrieving the typelem from pg_type tablecalculating the next enumsortorder number直接插入pg_enum表;

        以下是我使用的代码。 (插入前检查是否存在重复值(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 必须全部小写。

        现在这可以安全地写入您的数据库迁移脚本。

        【讨论】:

          【解决方案16】:

          最简单的:摆脱枚举。它们不容易修改,因此应该非常很少使用。

          【讨论】:

          • 也许一个简单的检查约束就可以了?
          • 将值存储为字符串到底有什么问题?
          • @Grazer:在 9.1 中,您可以将值添加到枚举 (depesz.com/index.php/2010/10/27/…) - 但您仍然无法删除旧值。
          • @WillSheppard - 认为基本上不会。我认为在任何情况下,基于带有检查约束的文本的自定义类型要好得多。
          • @JackDouglas - 当然。我会在任何一天通过检查枚举来获取域。
          【解决方案17】:

          使用 Navicat 时,您可以转到类型(在视图下 -> 其他 -> 类型) - 获取类型的设计视图 - 然后单击“添加标签”按钮。

          【讨论】:

          • 会很好,但在现实生活中,它没有用:ERROR: cannot drop type foo because other objects depend on it HINT: Use DROP ... CASCADE to drop the dependent objects too.
          • 很奇怪,它对我有用。 (不知道为什么当 TS 只想向枚举字段添加值时使用 DROP)
          • 我没有专门做 DROP,但完全按照您的程序进行。我假设 Navicat 在幕后做了 DROP 并且失败了。我正在使用 Navicat 9.1.5 Lite。
          【解决方案18】:

          我不知道是否有其他选择,但我们可以使用以下方法删除该值:

          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;
          

          【讨论】:

            猜你喜欢
            • 2022-07-13
            • 1970-01-01
            • 2015-11-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-10-28
            • 1970-01-01
            • 2019-04-14
            相关资源
            最近更新 更多