Aggiunta di un nuovo valore a un tipo ENUM esistente


208

Ho una colonna della tabella che utilizza un enumtipo. Vorrei aggiornare quel enumtipo per avere un ulteriore valore possibile. Non voglio eliminare alcun valore esistente, basta aggiungere il nuovo valore. Qual è il modo più semplice per farlo?

Risposte:


153

NOTA se stai usando PostgreSQL 9.1 o versioni successive e sei d'accordo con le modifiche al di fuori di una transazione, vedi questa risposta per un approccio più semplice.


Ho avuto lo stesso problema qualche giorno fa e ho trovato questo post. Quindi la mia risposta può essere utile per qualcuno che sta cercando una soluzione :)

Se hai solo una o due colonne che usano il tipo enum che vuoi cambiare, puoi provare questo. Inoltre è possibile modificare l'ordine dei valori nel nuovo tipo.

-- 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 dovrebbe essere ripetuto se c'è più di 1 colonna.


9
Vale la pena ricordare che tutto ciò può essere fatto in un'unica transazione, quindi è soprattutto sicuro farlo in un database di produzione.
David Leppik,

52
Questa non è mai stata una buona idea. Da 9.1 puoi fare tutto con ALTER TYPE. Ma anche prima, ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type;era di gran lunga superiore.
Erwin Brandstetter,

1
Ricorda che le versioni precedenti di Postgres non supportano i tipi di ridenominazione. In particolare la versione di Postgres su Heroku (db condiviso, credo che utilizzino PG 8.3) non la supporta.
Ortwin Gentz,

13
Puoi comprimere i passaggi 3, 4, 5 e 6 in un'unica istruzione:ALTER TABLE some_table ALTER COLUMN some_column TYPE some_enum_type USING some_column::text::some_enum_type;
glyphobet,

3
Se lo si fa su un tavolo attivo, bloccare il tavolo durante la procedura. Il livello di isolamento della transazione predefinito in postgresql non impedisce l'inserimento di nuove righe da altre transazioni durante questa transazione, quindi potresti rimanere con righe popolate in modo errato.
Sérgio Carvalho,

422

PostgreSQL 9.1 introduce capacità di ALTER tipi 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
che cos'è il "enum_type"? nome campo, nome campo_tabella? o qualcos'altro? come dovrei colpirlo? Ho una tabella "voti" e ho la colonna "tipo" E nel db dump ottengo questo: CONSTRAINT grades_type_check CHECK (((type) :: text = ANY ((ARRAY ['esame' :: carattere che varia, 'test': : variazione di carattere, 'extra' :: variazione di carattere, 'medio termine' :: variazione di carattere, 'finale' :: variazione di carattere]) :: testo [])))

1
enum_type è solo il tuo nome di tipo enum @mariotanenbaum. Se il tuo enum è un "tipo", questo è ciò che dovresti usare.
Dariusz,

26
è possibile rimuoverne uno?
Ced

8
Aggiungendo al commento di @DrewNoakes, se si utilizza db-migrate (che viene eseguito nella transazione), è possibile che venga visualizzato un errore: ERRORE: ALTER TYPE ... ADD non può essere eseguito all'interno di un blocco di transazione La soluzione è citata qui (da Hubbitus ): stackoverflow.com/a/41696273/1161370
Mahesh,

1
non è possibile rimuoverlo in modo da rendere impossibile la migrazione del dow, quindi è necessario ricorrere ad altri metodi
Muhammad Umer,

65

Una possibile soluzione è la seguente; condizione preliminare è che non vi siano conflitti nei valori enum usati. (ad es. quando si rimuove un valore enum, assicurarsi che questo valore non venga più utilizzato.)

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

Anche in questo modo l'ordine delle colonne non verrà modificato.


1
+1 questa è la strada da percorrere prima della 9.1 e ancora la strada da percorrere per cancellare o modificare elementi.

Questa è di gran lunga la risposta migliore per la mia soluzione, che aggiunge nuovi enum a un tipo di enum esistente, in cui manteniamo tutti i vecchi enum e ne aggiungiamo di nuovi. Inoltre, il nostro script di aggiornamento è transazionale. Ottimo post!
Darin Peterson,

1
Risposta brillante! Evita gli hack in giro pg_enumche possono effettivamente rompere le cose ed è transazionale, a differenza ALTER TYPE ... ADD.
NathanAldenSr

4
Nel caso in cui la vostra colonna ha un valore predefinito verrà visualizzato il seguente errore: default for column "my_column" cannot be cast automatically to type "my_enum". Dovrai fare quanto segue: 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

Se ti trovi in ​​una situazione in cui dovresti aggiungere enumvalori nella transazione, eseguilo nella migrazione flyway sulla ALTER TYPEdichiarazione che riceverai un errore ERROR: ALTER TYPE ... ADD cannot run inside a transaction block(vedi il numero 350 della flyway ) potresti aggiungere tali valori pg_enumdirettamente come soluzione alternativa ( type_egais_unitsè il nome del 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
Tuttavia, ciò richiederà la concessione delle autorizzazioni di amministratore, poiché cambia la tabella di sistema.
Asnelzin,

22

Completamento di @Dariusz 1

Per Rails 4.2.1, c'è questa sezione doc:

== Migrazioni transazionali

Se la scheda di database supporta le transazioni DDL, tutte le migrazioni verranno automaticamente inserite in una transazione. Esistono query che non è possibile eseguire all'interno di una transazione e per queste situazioni è possibile disattivare le transazioni automatiche.

class ChangeEnum < ActiveRecord::Migration
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end

3
Questo! se stai giocando con enumerazioni su binari moderni, questo è esattamente quello che stai cercando.
Eli Albert,

1
Ottimo, mi ha aiutato molto!
Dmytro Uhnichenko il

10

Dalla documentazione di Postgres 9.1 :

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

Esempio:

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

3
Anche dalla documentazione: i confronti che comportano un valore enum aggiunto a volte saranno più lenti rispetto ai confronti che coinvolgono solo membri originali del tipo enum. [.... dettagliato spezzato come troppo lungo per il commento di stackoverflow ...] Il rallentamento è generalmente insignificante; ma se è importante, è possibile ottenere prestazioni ottimali eliminando e ricreando il tipo enum oppure scaricando e ricaricando il database.
Aaron Zinman,

8

Disclaimer: non ho provato questa soluzione, quindi potrebbe non funzionare ;-)

Dovresti guardare pg_enum. Se vuoi solo cambiare l'etichetta di un ENUM esistente, un semplice AGGIORNAMENTO lo farà.

Per aggiungere un nuovo valore ENUM:

  • Innanzitutto inserire il nuovo valore in pg_enum. Se il nuovo valore deve essere l'ultimo, il gioco è fatto.
  • In caso contrario (è necessario un nuovo valore ENUM tra quelli esistenti), dovrai aggiornare ciascun valore distinto nella tabella, passando dal più alto al più basso ...
  • Quindi dovrai solo rinominarli pg_enum nell'ordine opposto.

Illustrazione
Sono disponibili le seguenti serie di etichette:

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

e vuoi ottenere:

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

poi:

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

poi:

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

E così via...



5

Non riesco a pubblicare un commento, quindi dirò solo che l'aggiornamento di pg_enum funziona in Postgres 8.4. Per come sono impostati i nostri enum, ho aggiunto nuovi valori ai tipi di enum esistenti tramite:

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

È un po 'spaventoso, ma ha senso dato il modo in cui Postgres memorizza effettivamente i suoi dati.


1
Bella risposta! Aiuta solo ad aggiungere un nuovo enum, ma ovviamente non risolve il caso in cui devi riordinare.
Mahmoud Abdelkader,


Insieme al carattere di sottolineatura iniziale per il nome tipografico, fanno anche distinzione tra maiuscole e minuscole. Ho quasi perso la testa cercando di selezionare per nome tipografico dalla tabella pg_type.
Mahesh

5

L'aggiornamento di pg_enum funziona, così come il trucco della colonna dell'intermediario evidenziato sopra. Si può anche usare USING magic per cambiare direttamente il tipo di colonna:

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;

Finché non hai funzioni che richiedono esplicitamente o restituiscono quell'enum, sei bravo. (pgsql si lamenterà quando si rilascia il tipo, se presente).

Inoltre, nota che PG9.1 sta introducendo un'istruzione ALTER TYPE, che funzionerà su enum:

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


La documentazione pertinente per PostgreSQL 9.1 è ora disponibile su 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;Ma ora in gran parte irrilevante ...
Erwin Brandstetter,

Allo stesso modo di quello che ha detto Erwin, ha ... USING bar::typefunzionato per me. Non ho nemmeno dovuto specificare ::text.
Daniel Werner,

3

Più semplice: sbarazzarsi di enumerazioni. Non sono facilmente modificabili e quindi dovrebbero essere usati molto raramente.


2
forse farà un semplice vincolo di controllo?

1
E qual è esattamente il problema di memorizzare valori come stringhe?

5
@Grazer: in 9.1 puoi aggiungere valori a enum ( depesz.com/index.php/2010/10/27/… ) - ma non puoi ancora rimuovere quelli vecchi.

3
@WillSheppard - Io credo che, in fondo mai. Penso che i tipi personalizzati basati sul testo con vincoli di controllo siano molto migliori in ogni caso.

3
@JackDouglas - certo. Prenderò dominio con check over enum ogni giorno.

3

Impossibile aggiungere un commento nella posizione appropriata, ma ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_typecon un valore predefinito nella colonna non riuscito. Dovevo:

ALTER table ALTER COLUMN bar DROP DEFAULT;

e poi ha funzionato.


3

per ogni evenienza, se stai usando Rails e hai diverse affermazioni dovrai eseguirle una per una, come:

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

1

Ecco una soluzione più generale ma piuttosto rapida, che oltre a cambiare il tipo stesso aggiorna tutte le colonne del database utilizzandolo. Il metodo può essere applicato anche se una nuova versione di ENUM è diversa da più di un'etichetta o manca alcune di quelle originali. Il codice seguente sostituisce my_schema.my_type AS ENUM ('a', 'b', 'c')con 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();

L'intero processo verrà eseguito abbastanza rapidamente, perché se l'ordine delle etichette persiste, non si verificherà alcun cambiamento effettivo dei dati. Ho applicato il metodo su 5 tabelle usando my_typee avendo 50.000-70.000 righe in ciascuna e l'intero processo ha richiesto solo 10 secondi.

Naturalmente, la funzione restituirà un'eccezione nel caso in cui le etichette mancanti nella nuova versione di ENUM vengano utilizzate da qualche parte nei dati, ma in tale situazione si dovrebbe comunque fare qualcosa in anticipo.


Questo è davvero prezioso. Il problema è con le viste che usano il vecchio ENUM, però. Devono essere eliminati e ricreati, il che è molto più complicato considerando altre viste a seconda di quelle eliminate. Non parliamo di tipi compositi ...
Ondřej Bouda,

1

Per coloro che cercano una soluzione in transazione, il seguente sembra funzionare.

Invece di an ENUM, a DOMAINdeve essere utilizzato sul tipo TEXTcon un vincolo che verifica che il valore sia compreso nell'elenco specificato di valori consentiti (come suggerito da alcuni commenti). L'unico problema è che nessun vincolo può essere aggiunto (e quindi né modificato) a un dominio se viene utilizzato da qualsiasi tipo composito (i documenti dicono semplicemente che "alla fine dovrebbe essere migliorato"). Tale restrizione può essere aggirata, tuttavia, usando un vincolo che chiama una funzione, come segue.

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;

In precedenza, ho usato una soluzione simile alla risposta accettata, ma è lungi dall'essere buona una volta considerate le viste o le funzioni o i tipi compositi (e soprattutto le viste che utilizzano altre viste usando gli ENUM modificati ...). La soluzione proposta in questa risposta sembra funzionare in qualsiasi condizione.

L'unico svantaggio è che non vengono eseguiti controlli sui dati esistenti quando vengono rimossi alcuni valori consentiti (il che potrebbe essere accettabile, soprattutto per questa domanda). (Una chiamata a ALTER DOMAIN test_domain VALIDATE CONSTRAINT val_checkfinire con lo stesso errore dell'aggiunta di un nuovo vincolo al dominio utilizzato da un tipo composito, sfortunatamente.)

Si noti che una leggera modifica come CHECK (value = ANY(get_allowed_values())), dove la get_allowed_values()funzione ha restituito l'elenco dei valori consentiti, non funzionerebbe, il che è abbastanza strano, quindi spero che la soluzione proposta sopra funzioni in modo affidabile (lo fa per me, finora ...). (funziona, in realtà - è stato un mio errore)


0

Come discusso in precedenza, il ALTERcomando non può essere scritto all'interno di una transazione. Il modo suggerito è quello di inserire direttamente nella tabella pg_enum, con retrieving the typelem from pg_type tableecalculating the next enumsortorder number ;

Di seguito è riportato il codice che utilizzo. (Verifica se esiste un valore duplicato prima dell'inserimento (vincolo tra enumtypid e nome 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'
    )

Si noti che il nome del tipo è preceduto da un trattino basso nella tabella pg_type. Inoltre, il nome tipografico deve essere tutto minuscolo nella clausola where.

Ora questo può essere scritto in modo sicuro nel tuo script di migrazione db.


-1

Non so se hanno altre opzioni ma possiamo eliminare il valore usando:

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

Quando si utilizza Navicat è possibile accedere ai tipi (in vista -> altri -> tipi) - ottenere la vista di progettazione del tipo - e fare clic sul pulsante "Aggiungi etichetta".


1
Sarebbe bello ma nella vita reale, non è utile:ERROR: cannot drop type foo because other objects depend on it HINT: Use DROP ... CASCADE to drop the dependent objects too.
Ortwin Gentz,

Strano, ha funzionato per me. (Non so perché usi DROP quando TS voleva solo aggiungere un valore al campo enum)
jvv

1
Non ho fatto un DROP specifico ma sono andato esattamente dopo la tua procedura. Presumo che Navicat faccia il DROP dietro le quinte e fallisca. Sto usando Navicat 9.1.5 Lite.
Ortwin Gentz,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.