Crea un vincolo PostgreSQL per impedire righe di combinazione univoche


9

Immagina di avere una tabella semplice:

name | is_active
----------------
A    | 0
A    | 0
B    | 0
C    | 1
...  | ...

Devo creare uno speciale vincolo unico che non riesce nella seguente situazione: is_activevalori diversi non possono coesistere per lo stesso namevalore.

Esempio di condizione consentita:

Nota: un semplice indice unico a più colonne non consentirà una combinazione come questa.

A    | 0
A    | 0
B    | 0

Esempio di condizione consentita:

A    | 0
B    | 1

Esempio di condizione non riuscita:

A    | 0
A    | 1
-- should be prevented, because `A 0` exists
-- same name, but different `is_active`

Idealmente, ho bisogno di un vincolo univoco o di un indice parziale univoco. I trigger sono più problematici per me.

Doppio A,0permesso, ma (A,0) (A,1)non lo è.

Risposte:


17

Puoi utilizzare un vincolo di esclusione con btree_gist,

-- This is needed
CREATE EXTENSION btree_gist;

Quindi aggiungiamo un vincolo che dice:

"Non possiamo avere 2 righe che hanno lo stesso namee diverso is_active" :

ALTER TABLE table_name
  ADD CONSTRAINT only_one_is_active_value_per_name
    EXCLUDE  USING gist
    ( name WITH =, 
      is_active WITH <>      -- if boolean, use instead:
                             -- (is_active::int) WITH <>
    );

Alcune note:

  • is_activepuò essere intero o booleano, non fa alcuna differenza per il vincolo di esclusione. (in realtà lo fa, se la colonna è booleana è necessario utilizzare (is_active::int) WITH <>.)
  • Le righe in cui nameo is_activeè null verranno ignorate dal vincolo e quindi consentite.
  • Il vincolo ha senso solo se la tabella ha più colonne. Altrimenti, se la tabella ha solo queste 2 colonne, un UNIQUEvincolo da (name)solo sarebbe più semplice e più appropriato. Non vedo alcun motivo per memorizzare più righe identiche.
  • Il design viola 2NF. Mentre il vincolo di esclusione ci salverà da anomalie di aggiornamento, potrebbe non causare problemi di prestazioni. Se ad esempio hai 1000 righe con name = 'A'e vuoi aggiornare lo stato is_active da 0 a 3, tutte le 1000 dovranno essere aggiornate. È necessario esaminare se la normalizzazione del progetto sarebbe più efficiente. (Normalizzazione del significato in questo caso per rimuovere lo stato is_active dalla tabella e aggiungere una tabella a 2 colonne con nome, is_active e un vincolo univoco attivo (name). Se is_activeè booleano, potrebbe essere completamente rimosso e la tabella aggiuntiva solo una tabella a colonna singola, memorizzando solo i nomi "attivi".)

is_active non può essere booleano,ERROR: data type boolean has no default operator class for access method "gist"
Evan Carroll il

1
@EvanCarroll Non ricordo quanto bene ho provato questo quando ho pubblicato. Ma funziona con inte smallint.
ypercubeᵀᴹ

Funziona anche EXCLUDE USING gist (name WITH =, (is_active::int) WITH <>)se è booleano. E la domanda ha 0e 1, no truee falsequindi è piuttosto improbabile che ho provato con i booleani;)
ypercubeᵀᴹ

Bene, ho usato un vincolo di esclusione su dba.stackexchange.com/a/175922/2639 e ho avuto un problema con l'uso di un valore booleano, quindi sono andato a cercare. Pensavo che btree_gist coprisse i bool ma non è così.
Evan Carroll,

3

Questo non è un caso in cui è possibile utilizzare un indice univoco. È possibile testare la condizione in un trigger, ad esempio:

create or replace function a_table_trigger()
returns trigger language plpgsql as $$
declare
    active int;
begin
    select is_active into active
    from a_table
    where name = new.name;

    if found and active is distinct from new.is_active then
        raise exception 'The value of is_active for "%" should be %', new.name, active;
    end if;
    return new;
end $$;

Provalo qui.

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.