354 votes

Créer une contrainte unique avec des colonnes nulles

J'ai une table avec cette disposition :

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 à celle-ci :

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

Cependant, cela permettra à plusieurs lignes avec le même nom d'utilisateur. (UserId, RecipeId) si MenuId IS NULL . Je veux permettre NULL en MenuId pour stocker un favori qui n'a pas de menu associé, mais je ne veux qu'un seul de ces rangs par couple utilisateur/préparation.

Les idées que j'ai jusqu'à présent sont :

  1. Utilisez un UUID codé en dur (comme tous les zéros) au lieu de null.
    Cependant, MenuId a une contrainte FK sur les menus de chaque utilisateur, ce qui m'obligerait à créer un menu spécial "nul" pour chaque utilisateur, ce qui est fastidieux.

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

  3. Il suffit de l'oublier et de vérifier l'existence préalable d'une entrée nulle dans le middle-ware ou dans une fonction d'insertion, et de ne pas avoir cette contrainte.

J'utilise Postgres 9.0.

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

0 votes

Comment se fait-il que l'on autorise plusieurs lignes avec le même ( UserId , RecipeId ), si MenuId IS NULL ?

1 votes

@Drux Je crois que depuis Null != Null il s'ensuit que (userid, recipieid, null) != (userid, recipieid, null) . Ainsi, les doublons seront autorisés qui nous semblent identiques, mais qui ne sont pas comparables à postgresql.

558voto

Erwin Brandstetter Points 110228

Créer 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 manière, il ne peut y avoir qu'une seule combinaison de (user_id, recipe_id) donde menu_id IS NULL ce qui permet de mettre en œuvre la contrainte souhaitée.

Inconvénients possibles :

  • Vous ne pouvez pas avoir une clé étrangère faisant référence à (user_id, menu_id, recipe_id) . (Il semble peu probable que vous souhaitiez une référence FK de trois colonnes de large - utilisez plutôt la colonne PK).
  • Vous ne pouvez pas vous baser CLUSTER sur un indice partiel.
  • Requêtes sans correspondance WHERE ne peut pas utiliser l'indice partiel.

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

Il s'agit d'une bonne solution pour un une seule colonne nullable peut-être pour deux. Mais cela devient vite incontrôlable pour un plus grand nombre, car il faut un index partiel distinct pour chaque combinaison de colonnes annulables, ce qui fait que le nombre augmente de façon binomiale. Pour colonnes multiples annulables voir plutôt :

D'ailleurs, je conseille de ne pas utiliser Identifiants en casse mixte dans PostgreSQL .

1 votes

@Erwin Brandsetter : concernant le " identifiants en majuscules et en minuscules "remarque : Tant qu'aucun guillemet double n'est utilisé, l'utilisation d'identificateurs à casse mixte est tout à fait acceptable. Il n'y a aucune différence dans l'utilisation d'identifiants entièrement en minuscules (encore une fois : seulement si aucun guillemet n'est utilisé)

18 votes

@a_horse_with_no_name : Je suppose que vous savez que je le sais. C'est en fait l'une des raisons pour lesquelles je conseille contre son utilisation. Les personnes qui ne connaissent pas aussi bien les spécificités peuvent être confuses, car dans les autres SGBDR, les identifiants sont (en partie) sensibles à la casse. Parfois, les gens s'embrouillent eux-mêmes. Ou ils construisent SQL dynamique et utiliser quote_ident() comme ils le devraient et oublier de passer les identifiants comme des chaînes de caractères minuscules maintenant ! N'utilisez pas d'identifiants en casse mixte dans PostgreSQL, si vous pouvez l'éviter. J'ai vu ici un certain nombre de demandes désespérées provenant de cette folie.

3 votes

@a_horse_with_no_name : Oui, c'est bien sûr vrai. Mais si vous pouvez les éviter : vous ne voulez pas d'identifiants en majuscules et en minuscules . Ils ne servent à rien. Si vous pouvez les éviter, ne les utilisez pas. En outre, elles sont tout simplement laides. Les identificateurs entre guillemets sont moches aussi. Les identificateurs SQL92 avec des espaces sont un faux pas fait par un comité. Ne les utilisez pas.

116voto

mu is too short Points 205090

Vous pourriez créer un index unique avec une coalescence sur le MenuId :

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

Il vous suffit de choisir un UUID pour le COALESCE qui ne se produira jamais dans la "vraie vie". Vous ne verrez probablement jamais un UUID nul dans la vie réelle, mais vous pourriez ajouter une contrainte CHECK si vous êtes paranoïaque (et puisque ils sont vraiment là pour vous attraper...) :

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

2 votes

Cela présente le défaut (théorique) qu'une entrée avec menu_id = '00000000-0000-0000-0000-000000000000' peut déclencher de fausses violations uniques - mais vous avez déjà abordé ce point dans votre commentaire.

2 votes

Je ne pense pas qu'un algorithme de génération d'UUID existant puisse donner cet UUID de toute façon :) Solution très créative.

2 votes

@Erwin : Oui, toutes les solutions basées sur les sentinelles souffrent de ce problème, un environnement UUID est l'un des rares endroits où je considérerais qu'il est assez sûr à utiliser. Si vous voulez être paranoïaque (ce qui est fortement recommandé) alors CHECK (MenuId is null or MenuId <> '00000000-0000-0000-0000-000000000000') pourrait être ajouté.

2voto

ypercube Points 62714

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)
)

0 votes

Une idée intéressante. Elle rend l'insertion un peu plus compliquée. Je devrais vérifier si une ligne existe déjà dans le fichier FavoriteWithoutMenu d'abord. Si c'est le cas, je me contente d'ajouter un lien vers le menu - sinon, je crée le fichier FavoriteWithoutMenu d'abord, puis le lier à un menu si nécessaire. Cela rend également très difficile la sélection de tous les favoris dans une seule requête : je devrais faire quelque chose de bizarre comme sélectionner d'abord tous les liens de menu, puis sélectionner tous les favoris dont les ID n'existent pas dans la première requête. Je ne suis pas sûr que cela me plaise.

0 votes

Je ne pense pas que l'insertion soit plus compliquée. Si vous voulez insérer un enregistrement avec NULL MenuId que vous insérez dans ce tableau. Sinon, au Favorites table. Mais pour les requêtes, oui, ce sera plus compliqué.

0 votes

En fait, la sélection de tous les favoris ne serait qu'une simple jointure GAUCHE pour obtenir le menu. Hmm oui, cela pourrait être la voie à suivre

-2voto

wildplasser Points 17900

Je pense qu'il y a un problème sémantique ici. A mon avis, un utilisateur peut avoir un (mais seulement un ) recette préférée pour préparer un menu spécifique. (L'OP a confondu menu et recette ; si je me trompe : veuillez interchanger MenuId et RecipeId ci-dessous) Cela implique que {user,menu} doit être une clé unique dans cette table. Et elle devrait pointer vers exactement un recette. Si l'utilisateur n'a pas de recette favorite pour ce menu spécifique aucune ligne ne devrait exister pour cette paire de clés {user,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 réduite de la table :

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 votes

Oui, c'est vrai. Sauf que, dans mon cas, je veux pouvoir avoir un favori qui n'appartient à aucun menu. Imaginez cela comme vos signets dans votre navigateur. Vous pouvez simplement "marquer" une page. Ou vous pouvez créer des sous-dossiers de signets et leur donner des noms différents. Je veux permettre aux utilisateurs de favoriser une recette, ou de créer des sous-dossiers de favoris appelés menus.

1 votes

Comme je l'ai dit : tout est question de sémantique. (Je pensais à la nourriture, évidemment) Avoir un favori "qui n'appartient à aucun menu" n'a aucun sens pour moi. On ne peut pas favoriser quelque chose qui n'existe pas, IMHO.

0 votes

Il semble qu'une normalisation de la base de données pourrait aider. Créez une deuxième table qui relie les recettes aux menus (ou non). Bien que cela généralise le problème et permette d'avoir plus d'un menu dont une recette pourrait faire partie. Quoi qu'il en soit, la question portait sur les index uniques dans PostgreSQL. Merci.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X