Contrainte unique multi-colonne et valeurs NULL de PostgreSQL


94

J'ai une table comme celle-ci:

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

Et je veux (id_A, id_B, id_C)être distinct dans n'importe quelle situation. Donc, les deux insertions suivantes doivent entraîner une erreur:

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

Mais il ne se comporte pas comme prévu car, selon la documentation, deux NULLvaleurs ne sont pas comparées, de sorte que les deux insertions passent sans erreur.

Comment puis-je garantir ma contrainte unique même si cela id_Cpeut être le NULLcas? En réalité, la vraie question est la suivante: puis-je garantir ce type d'unicité dans "pure sql" ou dois-je l'implémenter à un niveau supérieur (java dans mon cas)?


Alors, disons que vous avez des valeurs (1,2,1)et (1,2,2)dans les (A,B,C)colonnes. Faut- (1,2,NULL)il permettre l'ajout ou non d'un?
Ypercubeᵀᴹ

A et B ne peuvent pas être nuls mais C peut être nul ou une valeur entière positive. Donc (1,2,3) et (2,4, null) sont valides mais (null, 2,3) ou (1, null, 4) sont invalides. Et [(1,2, null), (1,2,3)] ne rompt pas la contrainte unique mais [(1,2, null), (1,2, null)] doit la rompre.
Manuel Leduc

2
Existe-t-il des valeurs qui n'apparaîtront jamais dans ces colonnes (comme des valeurs négatives?)
a_horse_with_no_name

Vous n'êtes pas obligé d'étiqueter vos contraintes dans pg. Cela va générer automatiquement un nom. Juste FYI.
Evan Carroll

Réponses:


94

Vous pouvez le faire en SQL pur . Créez un index unique partiel en plus de celui que vous avez:

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

De cette façon, vous pouvez entrer (a, b, c)dans votre tableau:

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

Mais rien de tout cela une seconde fois.

Ou utilisez deuxUNIQUE index partiels et aucun index complet (ou contrainte). La meilleure solution dépend des détails de vos besoins. Comparer:

Bien que cela soit élégant et efficace pour une seule colonne nullable dans l' UNIQUEindex, cela devient rapidement incontrôlable pour plus. Discuter de cela - et comment utiliser UPSERT avec des index partiels:

À part

Aucune utilisation pour les identificateurs de casse mixtes sans guillemets doubles dans PostgreSQL.

Vous pouvez considérer une serialcolonne comme clé primaire ou une IDENTITYcolonne dans Postgres 10 ou version ultérieure. Apparenté, relié, connexe:

Alors:

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

Si vous ne prévoyez pas plus de 2 milliards de lignes (> 2147483647) pendant la durée de vie de votre table (y compris les lignes inutilisées et supprimées), envisagez integer(4 octets) au lieu de bigint(8 octets).


1
Les docs recommandent cette méthode. L'ajout d'une contrainte unique créera automatiquement un index B-tree unique sur la colonne ou le groupe de colonnes répertoriées dans la contrainte. Une restriction d'unicité ne couvrant que certaines lignes ne peut pas être écrite en tant que contrainte unique, mais il est possible de l'appliquer en créant un index partiel unique.
Evan Carroll

12

J'ai eu le même problème et j'ai trouvé un autre moyen d'avoir un NULL unique dans la table.

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

Dans mon cas, le champ foreign_key_fieldest un entier positif et ne sera jamais -1.

Donc, pour répondre à Manuel Leduc, une autre solution pourrait être

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

Je suppose que les identifiants ne seront pas -1.

Quel est l'avantage de créer un index partiel?
Dans le cas où vous ne disposez pas de la clause NOT NULL, id_a, id_bet id_cpeut être NULL ensemble qu'une seule fois.
Avec un index partiel, les 3 champs peuvent être NULL plusieurs fois.


3
> Quel est l'avantage de créer un index partiel? La manière dont vous l'avez fait COALESCEpeut être efficace pour limiter les doublons, mais l'index ne serait pas très utile pour interroger car c'est un index d'expression qui ne correspond probablement pas aux expressions de requête. C'est-à-dire, à moins que SELECT COALESCE(col, -1) ...vous ne frappiez pas l'index.
Bo Jeanes

@BoJeanes L'index n'a pas été créé pour un problème de performances. Il a été créé pour répondre aux besoins de l'entreprise.
Luc M

8

Une valeur Null peut signifier que la valeur n'est pas connue pour cette ligne pour le moment mais qu'elle sera ajoutée ultérieurement, si elle est connue (exemple FinishDatepour une exécution Project) ou qu'aucune valeur ne peut être appliquée pour cette ligne (exemple EscapeVelocitypour un trou noir Star).

À mon avis, il est généralement préférable de normaliser les tables en éliminant toutes les valeurs nulles.

Dans votre cas, vous souhaitez autoriser NULLsvotre colonne, mais vous ne souhaitez NULLautoriser qu’une seule de ces colonnes . Pourquoi? Quel genre de relation est-ce entre les deux tables?

Peut-être que vous pouvez simplement changer la colonne NOT NULLet stocker, au lieu de NULL, une valeur spéciale (comme -1) connue pour ne jamais apparaître. Cela résoudra le problème de la contrainte d'unicité (mais peut avoir d'autres effets secondaires éventuellement indésirables. Par exemple, utiliser -1pour signifier "inconnu / ne s'applique pas" faussera toute somme ou tout calcul moyen de la colonne. Ou tous ces calculs devront prendre en tenant compte de la valeur spéciale et l'ignorer.)


2
Dans mon cas, NULL est vraiment NULL (id_C est une clé étrangère à table_c par exemple, donc elle ne peut pas avoir de valeur -1), cela signifie qu'il n'y a pas de relation entre "ma_table" et "table_c". Donc, cela a une signification fonctionnelle. En passant, [(1, 1,1, null), (2, 1,2, null), (3,2,4, null)] est une liste valide des données insérées.
Manuel Leduc

1
Ce n'est pas vraiment un Null tel qu'utilisé dans SQL car vous n'en voulez qu'une dans toutes les lignes. Vous pouvez modifier le schéma de votre base de données en ajoutant -1 à table_c ou en ajoutant une autre table (qui serait du type supertype du sous-type table_c).
Ypercubeᵀᴹ

3
Je voudrais simplement signaler à @Manuel que l'opinion sur les valeurs NULL dans cette réponse n'est pas universellement partagée et fait l'objet de nombreux débats. Beaucoup, comme moi, pensent que null peut être utilisé à toutes fins utiles (mais ne devrait signifier qu'une chose pour chaque champ et être documenté, éventuellement dans le nom du champ ou un commentaire de colonne)
Jack Douglas

1
Vous ne pouvez pas utiliser de valeur factice lorsque votre colonne est une clé étrangère.
Luc M

1
+1 Je suis avec vous: si nous voulons qu'une combinaison de colonnes soit unique, vous devez alors considérer une entité dans laquelle cette combinaison de colonnes est une clé publique. Le schéma de base de données des OP devrait probablement passer à une table parent et à une table enfant.
AK
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.