Vincolo univoco multi-colonna PostgreSQL e valori NULL


94

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 voglio (id_A, id_B, id_C)essere distinto 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 NULLvalori non vengono confrontati tra loro, quindi entrambi gli inserti passano senza errori.

Come posso garantire il mio vincolo unico anche se id_Cpuò esserlo NULLin 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)?


Quindi, supponi di avere dei valori (1,2,1)e (1,2,2)nelle (A,B,C)colonne. Dovrebbe (1,2,NULL)essere permesso di aggiungere o no?
ypercubeᵀᴹ

A e B non possono essere nulli ma C può essere nullo o qualsiasi valore intero positivo. Quindi (1,2,3) e (2,4, null) sono validi ma (null, 2,3) o (1, null, 4) non sono validi. E [(1,2, null), (1,2,3)] non rompe il vincolo univoco ma [(1,2, null), (1,2, null)] deve romperlo.
Manuel Leduc,

2
Ci sono valori che non compariranno mai in quelle colonne (come valori negativi?)
a_horse_with_no_name

Non è necessario etichettare i vincoli in pag. Genererà automagicamente un nome. Cordiali saluti.
Evan Carroll,

Risposte:


94

Puoi farlo in puro SQL . Crea un indice univoco parziale oltre 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 entrare per (a, b, c)nella tua tabella:

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

Ma nessuno di questi una seconda volta.

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

Mentre questo è elegante ed efficiente per una singola colonna nullable UNIQUEnell'indice, sfugge di mano rapidamente per altro. Discutendo questo e come usare UPSERT con indici parziali:

asides

Nessun uso per identificatori di casi misti senza virgolette doppie in PostgreSQL.

Si potrebbe prendere in considerazione una serialcolonna come chiave primaria o una IDENTITYcolonna in Postgres 10 o successivo. 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), considera integer(4 byte) anziché bigint(8 byte).


1
I documenti sostengono questo metodo, l' aggiunta di un vincolo univoco creerà automaticamente un indice B-tree univoco sulla colonna o sul gruppo di colonne elencate nel vincolo. Una restrizione di unicità che copre solo alcune righe non può essere scritta come un vincolo univoco, ma è possibile applicare tale restrizione creando un indice parziale univoco.
Evan Carroll,

12

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_be id_cpuò essere NULL insieme solo una volta.
Con un indice parziale, i 3 campi potrebbero essere NULL più di una volta.


3
> Qual è il vantaggio nella creazione di un indice parziale? Il modo in cui lo hai fatto COALESCEpuò essere efficace nel limitare i duplicati, ma l'indice non sarebbe molto utile nelle query in quanto è un indice di espressione che probabilmente non corrisponderà alle espressioni di query. Cioè, a meno che SELECT COALESCE(col, -1) ...tu non voglia colpire l'indice.
Bo Jeanes,

@BoJeanes L'indice non è stato creato per un problema di prestazioni. È stato creato per soddisfare i requisiti aziendali.
Luc M

8

Un valore Nullo può significare che il valore non è noto per quella riga al momento, ma verrà aggiunto, quando noto, in futuro (esempio FinishDateper una corsa Project) o che nessun valore può essere applicato per quella riga (esempio EscapeVelocityper un buco nero Star).

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

Nel tuo caso, si desidera consentire NULLsnella colonna, ma si desidera solo uno NULLper essere consentito. Perché? Che tipo di relazione esiste tra le due tabelle?

Forse puoi semplicemente cambiare la colonna NOT NULLe memorizzare, 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 -1per significare "non noto / non applicabile" si distorcono i calcoli di somma o media sulla colonna. O tutti questi calcoli dovranno prendere prendere in considerazione il valore speciale e ignorarlo.)


2
Nel mio caso NULL è davvero NULL (id_C è una chiave esterna di table_c per esempio, quindi non può avere un valore -1), significa che non c'è alcuna relazione tra "my_table" e "table_c". Quindi ha un significato funzionale. A proposito [(1, 1,1, null), (2, 1,2, null), (3,2,4, null)] è un elenco valido di dati inseriti.
Manuel Leduc,

1
Non è proprio un Null come usato in SQL perché ne vuoi solo uno in tutte le righe. È possibile modificare lo schema del database aggiungendo -1 a table_c o aggiungendo un'altra tabella (che sarebbe un sottotipo al sottotipo table_c).
ypercubeᵀᴹ

3
Vorrei solo sottolineare a @Manuel che l'opinione sui null in questa risposta non è universalmente condivisa ed è molto dibattuta. Molti, come me, pensano che null possa essere utilizzato per qualsiasi scopo tu voglia (ma dovrebbe significare solo una cosa per ogni campo ed essere documentato, possibilmente nel nome del campo o in un commento di colonna)
Jack Douglas

1
Non è possibile utilizzare un valore fittizio quando la colonna è un CHIAVE ESTERO.
Luc M,

1
+1 Sono con te: se vogliamo che una combinazione di colonne sia unica, devi considerare un'entità in cui questa combinazione di colonne è un PK. Lo schema del database dei PO dovrebbe probabilmente cambiare in una tabella padre e una tabella figlio.
AK,
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.