web-dev-qa-db-fra.com

Créer une contrainte unique avec des colonnes nulles

J'ai une table avec cette mise en page:

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

Je veux créer une contrainte unique semblable à ceci:

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

Cependant, cela autorisera plusieurs lignes avec le même (UserId, RecipeId), si MenuId IS NULL. Je souhaite autoriser NULL dans MenuId à stocker un favori auquel aucun menu ne soit associé, mais je souhaite au maximum une de ces lignes par paire utilisateur/recette.

Les idées que j'ai jusqu'ici sont:

  1. Utilisez des UUID codés en dur (tels que tous les zéros) au lieu de null.
    Cependant, MenuId a une contrainte FK sur les menus de chaque utilisateur, je devais alors créer un menu "null" spécial pour chaque utilisateur qui est un problème.

  2. Vérifiez l'existence d'une entrée NULL à l'aide d'un déclencheur.
    Je pense que c'est un problème et j'aime éviter les déclencheurs dans la mesure du possible. De plus, je ne leur fais pas confiance pour garantir que mes données ne sont jamais en mauvais état.

  3. Oubliez simplement cela et vérifiez l'existence antérieure d'une entrée null dans le middle-ware ou dans une fonction d'insertion et n'ayez pas cette contrainte.

J'utilise Postgres 9.0.

Y a-t-il une méthode que je néglige?

211
Mike Christensen

Créez deux index partiels :

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;

De cette façon, il ne peut y avoir qu'une seule combinaison de (user_id, recipe_id)menu_id IS NULL, mettant en œuvre efficacement la contrainte souhaitée.

Inconvénients possibles: vous ne pouvez pas avoir de clé étrangère référençant (user_id, menu_id, recipe_id), vous ne pouvez pas baser CLUSTER sur un index partiel et les requêtes sans condition WHERE correspondante ne peuvent pas utiliser l'index partiel. (Il semble peu probable que vous souhaitiez une référence FK de trois colonnes de large - utilisez plutôt la colonne PK).

Si vous avez besoin d'un index complet , vous pouvez également supprimer la condition WHERE de favo_3col_uni_idx et vos exigences sont toujours respectées.
L’indice, qui comprend maintenant tout le tableau, se superpose à l’autre et s’agrandit. En fonction de requêtes typiques et du pourcentage de valeurs NULL, cela peut être utile ou non. Dans des situations extrêmes, il peut même être utile de conserver les trois index (les deux partiels et un total en haut).

De plus: je déconseille d'utiliser identifiants de casse mixtes dans PostgreSQL .

328
Erwin Brandstetter

Vous pouvez créer un index unique avec une fusion sur le MenuId:

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

Vous aurez juste besoin de choisir un UUID pour la COALESCE qui ne se produira jamais dans la "vraie vie". Vous ne verriez probablement jamais un UUID nul dans la vie réelle mais vous pouvez ajouter une contrainte CHECK si vous êtes paranoïaque (et depuis ils sont vraiment à la recherche de vous ...):

alter table Favorites
add constraint check
(MenuId <> '00000000-0000-0000-0000-000000000000')
60
mu is too short

Vous pouvez stocker les favoris sans menu associé dans un tableau séparé:

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

Je pense qu'il y a un problème sémantique ici. À mon avis, un utilisateur peut avoir une recette préférée (mais ne seule) pour préparer un menu spécifique. (L'OP mélange le menu et la recette; si je me trompe, veuillez échanger les identificateurs MenuId et RecipeId ci-dessous) Cela implique que {utilisateur, menu} doit être une clé unique dans ce tableau. Et il devrait pointer sur exactement un recette. Si l'utilisateur n'a pas de recette favorite pour ce menu spécifique , aucune ligne ne doit exister pour cette paire de touches {utilisateur, menu}. De plus, la clé de substitution (FaVouRiteId) est superflue: les clés primaires composites sont parfaitement valables pour les tables de mappage relationnel.

Cela conduirait à la définition de table réduite:

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)
);
0
wildplasser