web-dev-qa-db-ja.com

既存のENUMタイプに新しい値を追加する

enumタイプを使用するテーブル列があります。 enumタイプを更新して、追加の可能な値を持つようにします。既存の値を削除するのではなく、新しい値を追加するだけです。これを行う最も簡単な方法は何ですか?

164
Ian

NOTEPostgreSQL 9.1以降を使用していて、トランザクションの外部で変更を行うことに問題がない場合は、 この回答を参照してください よりシンプルなアプローチ。


私は数日前に同じ問題を抱えていて、この投稿を見つけました。だから私の答えは、解決策を探している人に役立ちます:)

変更する列挙型を使用する列が1つまたは2つしかない場合は、これを試すことができます。また、新しいタイプの値の順序を変更できます。

-- 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を繰り返す必要があります。

130
taksofan

PostgreSQL9.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';
339
Dariusz

考えられる解決策は次のとおりです。前提条件は、使用される列挙値に競合がないことです。 (たとえば、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__;

また、この方法では、列の順序は変更されません。

60
Steffen

トランザクションにenum値を追加する必要がある状況に陥った場合、f.e。 ALTER TYPEステートメントのflyway移行で実行すると、エラーERROR: ALTER TYPE ... ADD cannot run inside a transaction blockが表示されます( flyway issue#35 を参照)回避策として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 )
27
Hubbitus

@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
13
Kiko Castro

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'
10
Peymankh

免責事項:私はこの解決策を試したことがないので、うまくいかないかもしれません;-)

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;

等々...

8
benja

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

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が実際にデータを保存する方法を考えると理にかなっています。

5
Josiah

適切な場所にコメントを追加することはできませんが、列にデフォルトがあるALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_typeは失敗しました。そうしなければならなかった:

ALTER table ALTER COLUMN bar DROP DEFAULT;

そしてそれは働いた。

3

最も単純な:列挙型を取り除きます。これらは簡単に変更できないため、veryはほとんど使用されません。

2
user80168

トランザクション内のソリューションを探している人には、次のように動作するようです。

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;

以前は、受け入れられた答えに似たソリューションを使用していましたが、ビューまたは関数または複合タイプ(特に、変更されたENUMを使用する他のビューを使用するビュー)が考慮されると、あまり良くありません。この回答で提案されているソリューションは、どのような条件下でも機能するようです。

唯一の欠点は、いくつかの許可された値が削除されたときに、既存のデータに対してチェックが実行されないことです(特にこの質問では受け入れられる場合があります)。 (残念ながら、ALTER DOMAIN test_domain VALIDATE CONSTRAINT val_checkへの呼び出しは、複合型で使用されるドメインに新しい制約を追加するのと同じエラーになります。)

CHECK (value = ANY(get_allowed_values()))関数(許可された値のリストを返すget_allowed_values()など)のようなわずかな変更は機能しないことに注意してください-これは非常に奇妙なので、上記のソリューションが確実に機能することを願っています、 これまでのところ...)。 (実際に動作します-それは私のエラーでした)

1
Ondřej Bouda

以下は、より一般的ではありますが、型の変更以外に、それを使用するデータベースのすべての列を更新する、かなり高速なソリューションです。 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();

ラベルの順序が続く場合、データの実際の変更は発生しないため、プロセス全体がかなり迅速に実行されます。 my_typeを使用し、各行に50,000〜70,000行を持つ5つのテーブルにメソッドを適用しました。プロセス全体にかかった時間はわずか10秒でした。

もちろん、新しいバージョンのENUMにないラベルがデータのどこかに使用されている場合、関数は例外を返しますが、そのような状況では、とにかく何かを事前に行う必要があります。

1

上記で説明したように、ALTERコマンドはトランザクション内に書き込むことはできません。推奨される方法は、retrieving the typelem from pg_type tableおよびcalculating 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テーブルでは、タイプ名の前にアンダースコアが付けられていることに注意してください。また、typnameはwhere句ですべて小文字である必要があります。

これで、db migrateスクリプトに安全に書き込むことができます。

0
Mahesh