it-swarm.it

Vincolo univoco multi-colonna PostgreSQL e valori NULL

Ho una tabella come la seguente:

create table my_table (
    id   int8 not null,
    id_A int8 not null,
    id_B int8 not null,
    id_C int8 null,
    constraint pk_my_table primary key (id),
    constraint u_constrainte unique (id_A, id_B, id_C)
);

E io voglio (id_A, id_B, id_C) essere distinti in ogni situazione. Pertanto i seguenti due inserti devono causare un errore:

INSERT INTO my_table VALUES (1, 1, 2, NULL);
INSERT INTO my_table VALUES (2, 1, 2, NULL);

Ma non si comporta come previsto perché, secondo la documentazione, due valori NULL non vengono confrontati tra loro, quindi entrambi gli inserti passano senza errori.

Come posso garantire il mio vincolo unico anche se id_C può essere NULL in questo caso? In realtà, la vera domanda è: posso garantire questo tipo di unicità in "sql puro" o devo implementarlo a un livello superiore (Java nel mio caso)?

102
Manuel Leduc

Puoi farlo in SQL puro . Crea un indice univoco parziale in aggiunta a quello che hai:

CREATE UNIQUE INDEX ab_c_null_idx ON my_table (id_A, id_B) WHERE id_C IS NULL;

In questo modo puoi inserire (a, b, c) nella tua tabella:

(1, 2, 1)
(1, 2, 2)
(1, 2, NULL)

Ma nessuno di questi una seconda volta.

Oppure usa due indici parziali UNIQUE e nessun indice (o vincolo) completo. La migliore soluzione dipende dai dettagli delle tue esigenze. Confrontare:

Sebbene sia elegante ed efficace per una singola colonna nullable nell'indice UNIQUE, si sfugge rapidamente per ulteriori informazioni. Discutendo questo e come usare UPSERT con indici parziali:

Asides

Inutile per identificatori di casi misti senza virgolette doppie in PostgreSQL.

Tu potrebbe consideri una serial colonna come chiave primaria o una IDENTITY colonna in Postgres 10 o dopo. Relazionato:

Così:

CREATE TABLE my_table (
   my_table_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY  -- for pg 10+
-- my_table_id bigserial PRIMARY KEY  -- for pg 9.6 or older
 , id_a int8 NOT NULL
 , id_b int8 NOT NULL
 , id_c int8
 , CONSTRAINT u_constraint UNIQUE (id_a, id_b, id_c)
);

Se non ti aspetti più di 2 miliardi di righe (> 2147483647) per tutta la durata della tabella (inclusi rifiuti e righe eliminate), prendi in considerazione integer (4 byte) anziché bigint (8 byte).

102

Ho avuto lo stesso problema e ho trovato un altro modo per avere NULL univoco nella tabella.

CREATE UNIQUE INDEX index_name ON table_name( COALESCE( foreign_key_field, -1) )

Nel mio caso, il campo foreign_key_field è un numero intero positivo e non sarà mai -1.

Quindi, per rispondere al manuale Leduc, potrebbe essere un'altra soluzione

CREATE UNIQUE INDEX  u_constrainte (COALESCE(id_a, -1), COALESCE(id_b,-1),COALESCE(id_c, -1) )

Suppongo che gli ID non saranno -1.

Qual è il vantaggio nella creazione di un indice parziale?
Nel caso in cui non si disponga della clausola NOT NULL, id_a, id_b e id_c può essere NULL insieme una sola volta.
Con un indice parziale, i 3 campi potrebbero essere NULL più di una volta.

12
Luc M

Un valore Null può significare che il valore non è noto per quella riga al momento, ma verrà aggiunto, quando noto, in futuro (esempio FinishDate per l'esecuzione Project) o che nessun valore può essere applicato per quella riga (esempio EscapeVelocity per un buco nero Star).

Secondo me, di solito è meglio normalizzare i tavoli eliminando tutti i Null.

Nel tuo caso, si desidera consentire NULLs nella colonna, ma si desidera consentire solo un NULL. Perché? Che tipo di relazione esiste tra le due tabelle?

Forse puoi semplicemente cambiare la colonna in NOT NULL e memorizza, invece di NULL, un valore speciale (come -1) che è noto per non apparire mai. Questo risolverà il problema del vincolo di unicità (ma potrebbe avere altri effetti collaterali eventualmente indesiderati. Ad esempio, usando -1 per indicare "non noto/non applicabile" inclinerà i calcoli di somma o media sulla colonna. O tutti questi calcoli dovranno tenere conto del valore speciale e ignorarlo.)

8
ypercubeᵀᴹ