Crea un vincolo univoco con colonne null


252

Ho una tabella con questo layout:

CREATE TABLE Favorites
(
  FavoriteId uuid NOT NULL PRIMARY KEY,
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  MenuId uuid
)

Voglio creare un vincolo unico simile a questo:

ALTER TABLE Favorites
ADD CONSTRAINT Favorites_UniqueFavorite UNIQUE(UserId, MenuId, RecipeId);

Tuttavia, ciò consentirà più righe con lo stesso (UserId, RecipeId), se MenuId IS NULL. Voglio permettere NULLa MenuIdmemorizzare un favorito quel menu non ha alcun associato, ma voglio solo al massimo una di queste righe per coppia utente / ricetta.

Le idee che ho finora sono:

  1. Usa un UUID hardcoded (come tutti gli zeri) invece di null.
    Tuttavia, MenuIdha un vincolo FK nei menu di ogni utente, quindi dovrei creare uno speciale menu "null" per ogni utente che è una seccatura.

  2. Verificare l'esistenza di una voce null utilizzando invece un trigger.
    Penso che questa sia una seccatura e mi piace evitare i trigger laddove possibile. Inoltre, non mi fido di loro per garantire che i miei dati non siano mai in cattivo stato.

  3. Basta dimenticarsene e verificare l'esistenza precedente di una voce null nel middleware o in una funzione di inserimento e non avere questo vincolo.

Sto usando Postgres 9.0.

C'è qualche metodo che sto trascurando?


Perché consentirà più righe con lo stesso ( UserId, RecipeId), se MenuId IS NULL?
Drux,

Risposte:


382

Crea due indici parziali :

CREATE UNIQUE INDEX favo_3col_uni_idx ON favorites (user_id, menu_id, recipe_id)
WHERE menu_id IS NOT NULL;

CREATE UNIQUE INDEX favo_2col_uni_idx ON favorites (user_id, recipe_id)
WHERE menu_id IS NULL;

In questo modo, può esserci solo una combinazione di (user_id, recipe_id)dove menu_id IS NULL, implementare efficacemente il vincolo desiderato.

Possibili inconvenienti: non è possibile fare riferimento a una chiave esterna (user_id, menu_id, recipe_id), non è possibile basarsi CLUSTERsu un indice parziale e le query senza una WHEREcondizione corrispondente non possono utilizzare l'indice parziale. (Sembra improbabile che tu voglia un riferimento FK largo tre colonne - usa invece la colonna PK).

Se è necessario un indice completo , è possibile in alternativa eliminare la WHEREcondizione favo_3col_uni_idxe i requisiti verranno comunque applicati.
L'indice, che comprende ora l'intero tavolo, si sovrappone all'altro e diventa più grande. A seconda delle query tipiche e della percentuale di NULLvalori, questo può o non può essere utile. In situazioni estreme potrebbe anche aiutare a mantenere tutti e tre gli indici (i due parziali e un totale in cima).

A parte: consiglio di non usare identificatori di casi misti in PostgreSQL .


1
@Erwin Brandsetter: per quanto riguarda l' osservazione degli " identificatori di casi misti ": fintanto che non vengono utilizzate virgolette doppie, l'uso di identificatori di maiuscole e minuscole va assolutamente bene. Non vi è alcuna differenza nell'uso di tutti gli identificatori minuscoli (di nuovo: solo se non vengono utilizzate le virgolette)
a_horse_with_no_name

14
@a_horse_with_no_name: suppongo tu sappia che lo so. Questo è in realtà uno dei motivi che mi consigliano contro di essa l'utilizzo. Le persone che non conoscono così bene le specifiche si confondono, come in altri identificatori RDBMS sono (parzialmente) sensibili al maiuscolo / minuscolo. A volte le persone si confondono. Oppure creano SQL dinamico e usano quote_ident () come dovrebbero e dimenticano di passare gli identificatori come stringhe minuscole ora! Non usare identificatori di casi misti in PostgreSQL, se puoi evitarlo. Ho visto un numero di richieste disperate qui derivanti da questa follia.
Erwin Brandstetter,

3
@a_horse_with_no_name: Sì, questo è ovviamente vero. Ma se puoi evitarli: non vuoi identificatori di casi misti . Non servono a nulla. Se puoi evitarli: non usarli. Inoltre: sono semplicemente brutti. Anche gli identificativi citati sono brutti. Gli identificatori SQL92 con spazi all'interno sono un passo falso fatto da un comitato. Non usarli.
wildplasser,

2
@Mike: penso che dovresti parlarne con il comitato degli standard SQL, buona fortuna :)
mu è troppo corto il

1
@buffer: i costi di manutenzione e lo stoccaggio totale sono sostanzialmente gli stessi (ad eccezione di un overhead fisso minore per indice). Ogni riga è rappresentata solo in un indice. Prestazioni: se i risultati si estendono in entrambi i casi, potrebbe essere pagato un indice normale totale aggiuntivo. In caso contrario, un indice parziale è in genere più veloce di un indice completo, principalmente a causa delle dimensioni inferiori. Aggiungi la condizione dell'indice alle query (in modo ridondante) se Postgres non capisce che può utilizzare un indice parziale da solo. Esempio.
Erwin Brandstetter,

75

È possibile creare un indice univoco con una coalescenza nel MenuId:

CREATE UNIQUE INDEX
Favorites_UniqueFavorite ON Favorites
(UserId, COALESCE(MenuId, '00000000-0000-0000-0000-000000000000'), RecipeId);

Dovresti solo scegliere un UUID per la COALESCE che non si verificherà mai nella "vita reale". Probabilmente non vedresti mai un UUID zero nella vita reale ma potresti aggiungere un vincolo CHECK se sei paranoico (e dal momento che sono davvero pronti a prenderti ...):

alter table Favorites
add constraint check
(MenuId <> '00000000-0000-0000-0000-000000000000')

1
Ciò comporta il difetto (teorico) che una voce con menu_id = '00000000-0000-0000-0000-000000000000' può innescare violazioni univoche false - ma l'hai già risolto nel tuo commento.
Erwin Brandstetter,

2
@muistooshort: Sì, questa è una soluzione adeguata. Semplifica (MenuId <> '00000000-0000-0000-0000-000000000000')però. NULLè consentito per impostazione predefinita. A proposito, ci sono tre tipi di persone. I paranoici e le persone che non fanno database. Il terzo tipo pubblica occasionalmente domande su SO con stupore. ;)
Erwin Brandstetter il

2
@Erwin: non intendi "quelli paranoici e quelli con database rotti"?
mu è troppo corto il

2
Questa eccellente soluzione semplifica l'inclusione di una colonna nulla di tipo più semplice, come intero, in un vincolo univoco.
Markus Pscheidt,

2
È vero che un UUID non produrrà quella particolare stringa, non solo a causa delle probabilità coinvolte, ma anche perché non è un UUID valido . Un generatore UUID non è libero di utilizzare alcuna cifra esadecimale in qualsiasi posizione, ad esempio una posizione è riservata al numero di versione dell'UUID.
Toby 1 Kenobi,

1

È possibile memorizzare i preferiti senza menu associato in una tabella separata:

CREATE TABLE FavoriteWithoutMenu
(
  FavoriteWithoutMenuId uuid NOT NULL, --Primary key
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  UNIQUE KEY (UserId, RecipeId)
)

Un'idea interessante Rende l'inserimento un po 'più complicato. Dovrei verificare se esiste già una riga per FavoriteWithoutMenuprima. In tal caso, aggiungo solo un collegamento al menu, altrimenti creo FavoriteWithoutMenuprima la riga e, se necessario, la collego a un menu. Rende anche molto difficile la selezione di tutti i preferiti in una query: dovrei fare qualcosa di strano come selezionare prima tutti i collegamenti di menu, quindi selezionare tutti i Preferiti i cui ID non esistono nella prima query. Non sono sicuro che mi piaccia.
Mike Christensen,

Non penso che l'inserimento sia più complicato. Se si desidera inserire un record con NULL MenuId, si inserisce in questa tabella. In caso contrario, al Favoritestavolo. Ma interrogare, sì, sarà più complicato.
ypercubeᵀᴹ

In realtà grattalo, selezionando tutti i preferiti sarebbe solo un singolo join SINISTRO per ottenere il menu. Sì, questo potrebbe essere il modo di andare ..
Mike Christensen,

L'INSERIMENTO diventa più complicato se si desidera aggiungere la stessa ricetta a più di un menu, poiché si ha un vincolo UNICO su UserId / RecipeId su FavoriteWithoutMenu. Dovrei creare questa riga solo se non esistesse già.
Mike Christensen,

1
Grazie! Questa risposta merita un +1 poiché è più una cosa SQL pura tra più database. Tuttavia, in questo caso andrò sul percorso dell'indice parziale perché non richiede modifiche al mio schema e mi piace :)
Mike Christensen

-1

Penso che ci sia un problema semantico qui. A mio avviso, un utente può avere una (ma solo una ) ricetta preferita per preparare un menu specifico. (L'OP ha menu e ricette confusi; in caso di errore: scambiare MenuId e RecipeId di seguito) Ciò implica che {utente, menu} dovrebbe essere una chiave univoca in questa tabella. E dovrebbe puntare esattamente a una ricetta. Se l'utente non ha una ricetta preferita per questo menu specifico, non dovrebbe esistere alcuna riga per questa coppia di chiavi {user, menu}. Inoltre: la chiave surrogata (FaVouRiteId) è superflua: le chiavi primarie composite sono perfettamente valide per le tabelle di mappatura relazionale.

Ciò porterebbe alla definizione ridotta della tabella:

CREATE TABLE Favorites
( UserId uuid NOT NULL REFERENCES users(id)
, MenuId uuid NOT NULL REFERENCES menus(id)
, RecipeId uuid NOT NULL REFERENCES recipes(id)
, PRIMARY KEY (UserId, MenuId)
);

2
Sì, è giusto. Tranne, nel mio caso, voglio supportare avere un preferito che non appartiene a nessun menu. Immaginalo come i tuoi segnalibri nel tuo browser. Potresti semplicemente "aggiungere un segnalibro" a una pagina. In alternativa, è possibile creare sottocartelle di segnalibri e denominarle in modo diverso. Voglio consentire agli utenti di aggiungere una ricetta ai preferiti o creare sottocartelle di preferiti chiamati menu.
Mike Christensen,

1
Come ho detto: si tratta di semantica. (Stavo pensando al cibo, ovviamente) Avere un preferito "che non appartiene a nessun menu" non ha senso per me. Non puoi favorire qualcosa che non esiste, IMHO.
wildplasser,

Sembra che un po 'di normalizzazione del db possa essere d'aiuto. Crea una seconda tabella che collega le ricette ai menu (o meno). Sebbene generalizzi il problema e consenta più di un menu di cui potrebbe far parte una ricetta. Indipendentemente da ciò, la domanda riguardava gli indici univoci in PostgreSQL. Grazie.
Chris,
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.