Menambahkan nilai baru ke Tipe ENUM yang ada

208

Saya memiliki kolom tabel yang menggunakan enumtipe. Saya ingin memperbarui enumjenis itu untuk memiliki nilai tambah yang mungkin. Saya tidak ingin menghapus nilai yang ada, cukup tambahkan nilai baru. Apa cara paling sederhana untuk melakukan ini?

Ian
sumber

Jawaban:

153

CATATAN jika Anda menggunakan PostgreSQL 9.1 atau lebih baru, dan Anda setuju dengan perubahan di luar transaksi, lihat jawaban ini untuk pendekatan yang lebih sederhana.


Saya memiliki masalah yang sama beberapa hari yang lalu dan menemukan posting ini. Jadi jawaban saya dapat bermanfaat bagi seseorang yang mencari solusi :)

Jika Anda hanya memiliki satu atau dua kolom yang menggunakan tipe enum yang ingin Anda ubah, Anda dapat mencoba ini. Anda juga dapat mengubah urutan nilai dalam tipe baru.

-- 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 harus diulang jika ada lebih dari 1 kolom.

taksofan
sumber
9
Perlu disebutkan bahwa ini semua dapat dilakukan dalam satu transaksi, jadi sebagian besar aman untuk melakukannya dalam database produksi.
David Leppik
52
Ini bukan ide yang bagus. Sejak 9,1 Anda dapat melakukan semuanya dengan ALTER TYPE. Tetapi bahkan sebelum itu, ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type;jauh lebih unggul.
Erwin Brandstetter
1
Ketahuilah bahwa versi Postgres yang lebih lama tidak mendukung jenis penggantian nama. Khususnya versi Postgres di Heroku (shared db, saya yakin mereka menggunakan PG 8.3) tidak mendukungnya.
Ortwin Gentz
13
Anda dapat menutup langkah 3, 4, 5 dan 6 menjadi satu pernyataan:ALTER TABLE some_table ALTER COLUMN some_column TYPE some_enum_type USING some_column::text::some_enum_type;
glyphobet
3
Jika melakukan ini di meja langsung, kunci meja selama prosedur. Level isolasi transaksi default di postgresql tidak akan mencegah baris baru dimasukkan oleh transaksi lain selama transaksi ini, sehingga Anda mungkin dibiarkan dengan baris yang salah jumlah populasinya.
Sérgio Carvalho
422

PostgreSQL 9.1 memperkenalkan kemampuan untuk mengubah tipe ENTER:

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';
Dariusz
sumber
1
apa itu "enum_type"? nama bidang, nama table_field? atau sesuatu yang lain? bagaimana saya bisa mengenai itu? Saya memiliki tabel "nilai" dan saya memiliki kolom "tipe" Dan di db dump saya mendapatkan ini: CONSTRAINT values_type_check PERIKSA ((tipe) :: teks = APA SAJA ((ARRAY ['ujian' :: karakter bervariasi, 'tes': : karakter bervariasi, 'ekstra' :: karakter bervariasi, 'midterm' :: karakter bervariasi, 'final' :: karakter bervariasi]) :: teks [])))
1
enum_type hanyalah nama jenis enum Anda sendiri @mariotanenbaum. Jika Anda enum Anda adalah "tipe" maka ini yang harus Anda gunakan.
Dariusz
26
apakah mungkin untuk menghapus satu?
Ced
8
Menambah komentar @DrewNoakes, jika Anda menggunakan db-migrasi (yang berjalan dalam transaksi), maka Anda mungkin mendapatkan kesalahan: ERROR: ALTER TYPE ... ADD tidak dapat berjalan di dalam blok transaksi. Solusinya disebutkan di sini (oleh Hubbitus ): stackoverflow.com/a/41696273/1161370
Mahesh
1
Anda tidak dapat menghapusnya sehingga membuat migrasi dow menjadi mustahil sehingga harus menggunakan metode lain
Muhammad Umer
65

Solusi yang mungkin adalah sebagai berikut; prasyaratnya adalah, bahwa tidak ada konflik dalam nilai enum yang digunakan. (mis. saat menghapus nilai enum, pastikan bahwa nilai ini tidak digunakan lagi.)

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

Juga dengan cara ini urutan kolom tidak akan diubah.

Steffen
sumber
1
+1 ini adalah cara untuk melanjutkan pra-9.1 dan masih cara untuk menghapus atau memodifikasi elemen.
Sejauh ini, inilah jawaban terbaik untuk solusi saya, yang menambahkan enum baru ke tipe enum yang ada, di mana kami menyimpan semua enum lama dan menambahkan yang baru. Selain itu skrip pembaruan kami bersifat transaksional. Pos yang bagus!
Darin Peterson
1
Jawaban yang brilian! Menghindari peretasan pg_enumyang benar-benar dapat merusak sesuatu dan bersifat transaksional, tidak seperti ALTER TYPE ... ADD.
NathanAldenSr
4
Dalam kasus kolom memiliki nilai default Anda akan menerima galat berikut: default for column "my_column" cannot be cast automatically to type "my_enum". Anda harus melakukan yang berikut: 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

Jika Anda jatuh ke dalam situasi ketika Anda harus menambahkan enumnilai dalam transaksi, maka jalankan dalam migrasi jalur terbang pada ALTER TYPEpernyataan Anda akan mendapatkan kesalahan ERROR: ALTER TYPE ... ADD cannot run inside a transaction block(lihat masalah jalur terbang # 350 ) Anda dapat menambahkan nilai-nilai tersebut pg_enumsecara langsung sebagai solusi ( type_egais_unitsadalah nama 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 )
Hubbitus
sumber
9
Namun, ini akan memerlukan pemberian izin admin, karena itu mengubah tabel sistem.
asnelzin
22

Melengkapi @Dariusz 1

Untuk Rails 4.2.1, ada bagian dokumen ini:

== Migrasi Transaksional

Jika adaptor database mendukung transaksi DDL, semua migrasi akan secara otomatis dibungkus dalam suatu transaksi. Ada pertanyaan yang tidak dapat Anda lakukan di dalam suatu transaksi, dan untuk situasi ini Anda dapat mematikan transaksi otomatis.

class ChangeEnum < ActiveRecord::Migration
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end
Kiko Castro
sumber
3
ini! jika Anda bermain dengan enum di rel modern, inilah yang Anda cari.
Eli Albert
1
Hebat, banyak membantu saya!
Dmytro Uhnichenko
10

Dari Postgres 9.1 Dokumentasi :

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

Contoh:

ALTER TYPE user_status ADD VALUE 'PROVISIONAL' AFTER 'NORMAL'
Peymankh
sumber
3
Juga dari dokumentasi: Perbandingan yang melibatkan nilai tambah enum terkadang akan lebih lambat daripada perbandingan yang hanya melibatkan anggota asli dari jenis enum. [.... terinci terpotong terlalu panjang untuk komentar stackoverflow ...] Perlambatan biasanya tidak signifikan; tetapi jika itu penting, kinerja optimal dapat diperoleh kembali dengan menjatuhkan dan menciptakan kembali tipe enum, atau dengan membuang dan memuat kembali basis data.
Aaron Zinman
8

Penafian: Saya belum mencoba solusi ini, jadi mungkin tidak berhasil ;-)

Anda harus melihat pg_enum. Jika Anda hanya ingin mengubah label ENUM yang ada, UPDATE sederhana akan melakukannya.

Untuk menambahkan nilai ENUM baru:

  • Pertama, masukkan nilai baru ke dalam pg_enum . Jika nilai baru harus menjadi yang terakhir, Anda sudah selesai.
  • Jika tidak (Anda perlu nilai ENUM baru di antara yang sudah ada), Anda harus memperbarui setiap nilai yang berbeda di tabel Anda, dari yang paling atas ke yang paling rendah ...
  • Maka Anda hanya perlu mengubah nama mereka dalam pg_enumurutan yang berlawanan.

Ilustrasi
Anda memiliki serangkaian label berikut:

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

dan Anda ingin mendapatkan:

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

kemudian:

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

kemudian:

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

Dan seterusnya...

benja
sumber
5

Sepertinya saya tidak bisa memposting komentar, jadi saya hanya akan mengatakan bahwa memperbarui pg_enum berfungsi di Postgres 8.4. Untuk cara enum kami diatur, saya telah menambahkan nilai baru ke tipe enum yang ada melalui:

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

Agak menakutkan, tetapi masuk akal mengingat cara Postgres menyimpan datanya.

Josiah
sumber
1
Jawaban bagus! Membantu hanya untuk menambahkan enum baru, tetapi jelas tidak menyelesaikan kasus di mana Anda harus memesan ulang.
Mahmoud Abdelkader
Seiring dengan garis bawah terkemuka untuk nama ketik, mereka juga peka huruf besar-kecil. Aku hampir kehilangan akal untuk mencoba memilih dengan mengetikkan nama dari tabel pg_type.
Mahesh
5

Memperbarui pg_enum berfungsi, seperti halnya trik kolom perantara yang disorot di atas. Anda juga dapat menggunakan sihir USING untuk mengubah jenis kolom secara langsung:

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;

Selama Anda tidak memiliki fungsi yang secara eksplisit membutuhkan atau mengembalikan enum itu, Anda baik. (pgsql akan mengeluh ketika Anda menjatuhkan tipenya jika ada.)

Juga, perhatikan bahwa PG9.1 memperkenalkan pernyataan ALTER TYPE, yang akan bekerja pada enum:

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

Denis de Bernardy
sumber
Dokumentasi yang relevan untuk PostgreSQL 9.1 sekarang dapat ditemukan di 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;Tapi sekarang sebagian besar tidak relevan ...
Erwin Brandstetter
Demikian pula dengan apa yang dikatakan Erwin, ... USING bar::typebekerja untuk saya. Saya bahkan tidak perlu menentukan ::text.
Daniel Werner
3

Sederhana: singkirkan enum. Mereka tidak mudah dimodifikasi, dan karenanya sangat jarang digunakan.


sumber
2
mungkin batasan pemeriksaan sederhana akan dilakukan?
1
Dan apa sebenarnya masalah menyimpan nilai sebagai string?
5
@Grazer: di 9.1 Anda bisa menambahkan nilai ke enum ( depesz.com/index.php/2010/10/27/… ) - tetapi Anda masih tidak bisa menghapus yang lama.
3
@ WillSheppard - Saya pikir pada dasarnya tidak pernah. Saya pikir jenis kustom berdasarkan teks dengan kendala periksa jauh lebih baik dalam hal apa pun.
3
@JackDouglas - yakin. Saya akan mengambil domain dengan cek atas enum setiap hari.
3

Tidak dapat menambahkan komentar ke tempat yang sesuai, tetapi ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_typegagal pada kolom default. Saya harus:

ALTER table ALTER COLUMN bar DROP DEFAULT;

dan kemudian berhasil.

Judy Morgan Loomis
sumber
3

untuk berjaga-jaga, jika Anda menggunakan Rails dan Anda memiliki beberapa pernyataan, Anda perlu mengeksekusi satu per satu, seperti:

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

Berikut ini adalah solusi yang lebih umum tetapi lebih cepat bekerja, yang selain mengubah tipe itu sendiri memperbarui semua kolom dalam database yang menggunakannya. Metode ini dapat diterapkan bahkan jika versi baru ENUM berbeda dengan lebih dari satu label atau melewatkan beberapa yang asli. Kode di bawah ini diganti my_schema.my_type AS ENUM ('a', 'b', 'c')dengan 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();

Seluruh proses akan berjalan cukup cepat, karena jika urutan label tetap ada, tidak ada perubahan data aktual yang akan terjadi. Saya menerapkan metode pada 5 tabel menggunakan my_typedan memiliki 50.000−70.000 baris di masing-masing, dan seluruh proses hanya membutuhkan waktu 10 detik.

Tentu saja, fungsi akan mengembalikan pengecualian jika label yang hilang dalam versi baru ENUM digunakan di suatu tempat dalam data, tetapi dalam situasi seperti itu, sesuatu harus dilakukan terlebih dahulu.

Alexander Kachkaev
sumber
Ini sangat berharga. Masalahnya adalah dengan tampilan menggunakan ENUM lama. Mereka harus dijatuhkan dan diciptakan kembali, yang jauh lebih rumit mengingat pandangan lain tergantung pada yang dijatuhkan. Tidak berbicara tentang tipe komposit ...
Ondřej Bouda
1

Bagi mereka yang mencari solusi dalam transaksi, berikut ini tampaknya berhasil.

Alih-alih a ENUM, a DOMAINharus digunakan pada tipe TEXTdengan kendala yang memeriksa bahwa nilainya berada dalam daftar nilai-nilai yang diizinkan (seperti yang disarankan oleh beberapa komentar). Satu-satunya masalah adalah tidak ada kendala yang dapat ditambahkan (dan karenanya tidak dimodifikasi) ke domain jika digunakan oleh tipe komposit apa pun (dokumen hanya mengatakan ini "pada akhirnya harus ditingkatkan"). Pembatasan semacam itu dapat dilakukan, dengan menggunakan batasan memanggil fungsi, sebagai berikut.

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;

Sebelumnya, saya menggunakan solusi yang mirip dengan jawaban yang diterima, tetapi masih jauh dari baik sekali dilihat atau fungsi atau tipe komposit (dan terutama pandangan menggunakan pandangan lain menggunakan ENUM yang dimodifikasi ...) dianggap. Solusi yang diajukan dalam jawaban ini tampaknya berfungsi dalam kondisi apa pun.

Satu-satunya kelemahan adalah bahwa tidak ada pemeriksaan yang dilakukan pada data yang ada ketika beberapa nilai yang diizinkan dihapus (yang mungkin dapat diterima, terutama untuk pertanyaan ini). (Panggilan untuk ALTER DOMAIN test_domain VALIDATE CONSTRAINT val_checkmengakhiri dengan kesalahan yang sama seperti menambahkan kendala baru ke domain yang digunakan oleh tipe komposit, sayangnya.)

Perhatikan bahwa sedikit modifikasi seperti CHECK (value = ANY(get_allowed_values())), di mana get_allowed_values()fungsi mengembalikan daftar nilai yang diizinkan, tidak akan berfungsi - yang cukup aneh, jadi saya harap solusi yang diusulkan di atas berfungsi dengan andal (itu berlaku untuk saya, sejauh ini ...). (Berhasil, sebenarnya - itu adalah kesalahan saya)

Ondřej Bouda
sumber
0

Seperti dibahas di atas, ALTERperintah tidak dapat ditulis di dalam suatu transaksi. Cara yang disarankan adalah menyisipkan ke dalam tabel pg_enum secara langsung, oleh retrieving the typelem from pg_type tabledan calculating the next enumsortorder number;

Berikut ini adalah kode yang saya gunakan. (Memeriksa apakah ada nilai duplikat sebelum memasukkan (batasan antara nama enumtypid dan 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'
    )

Perhatikan bahwa nama tipe Anda diawali dengan garis bawah pada tabel pg_type. Juga, nama samaran harus semuanya huruf kecil dalam klausa where.

Sekarang ini dapat ditulis dengan aman ke skrip migrasi db Anda.

Mahesh
sumber
-1

Saya tidak tahu apakah memiliki opsi lain tetapi kami dapat menurunkan nilainya menggunakan:

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;
Jardel
sumber
-2

Saat menggunakan Navicat Anda dapat pergi ke jenis (di bawah tampilan -> orang lain -> jenis) - dapatkan tampilan desain jenis - dan klik tombol "tambahkan label".

jvv
sumber
1
Akan menyenangkan tetapi dalam kehidupan nyata, itu tidak berguna:ERROR: cannot drop type foo because other objects depend on it HINT: Use DROP ... CASCADE to drop the dependent objects too.
Ortwin Gentz
Aneh, itu berhasil untuk saya. (Tidak yakin mengapa Anda menggunakan DROP ketika TS hanya ingin menambahkan nilai ke bidang enum)
jvv
1
Saya tidak melakukan DROP secara khusus tetapi langsung pergi setelah prosedur Anda. Saya berasumsi Navicat melakukan DROP di belakang layar dan gagal. Saya menggunakan Navicat 9.1.5 Lite.
Ortwin Gentz