web-dev-qa-db-fra.com

Contrainte unique multi-colonnes PostgreSQL et valeurs NULL

J'ai un tableau comme celui-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 toutes les situations. Par conséquent, 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 valeurs NULL ne sont pas comparées, donc les deux insertions passent sans erreur.

Comment puis-je garantir ma contrainte unique même si id_C peut être NULL dans ce cas? En fait, la vraie question est: est-ce que je peux garantir ce type d'unicité en "pure sql" ou dois-je l'implémenter à un niveau supérieur (Java dans mon cas)?

102
Manuel Leduc

Vous pouvez le faire en SQL pur . Créez un index unique partiel en plus à 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 pour (a, b, c) dans votre table:

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

Mais rien de tout cela une deuxième fois.

Ou utilisez deux index partiels UNIQUE 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'index UNIQUE, il devient rapidement incontrôlable pour plus. Discuter de cela - et comment utiliser UPSERT avec des index partiels:

À part

Pas d'utilisation pour identificateurs de casse mixte sans guillemets doubles dans PostgreSQL.

Vous pourrait considérer une colonne serial comme clé primaire ou une colonne IDENTITY dans Postgres 10 ou plus tard. En relation:

Donc:

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 vous attendez pas à plus de 2 milliards de lignes (> 2147483647) au cours de la durée de vie de votre table (y compris les déchets et les lignes supprimées), envisagez integer (4 octets) au lieu de bigint (8 octets).

102
Erwin Brandstetter

J'ai eu le même problème et j'ai trouvé une autre façon 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_field est un entier positif et ne sera jamais -1.

Donc, pour répondre à Manual 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 n'avez pas la clause NOT NULL, id_a, id_b et id_c ne peut être NULL ensemble qu'une seule fois.
Avec un index partiel, les 3 champs peuvent être NULL plusieurs fois.

12
Luc M

Un Null peut signifier que la valeur n'est pas connue pour cette ligne pour le moment mais sera ajoutée, lorsqu'elle sera connue, à l'avenir (exemple FinishDate pour un Project en cours d'exécution) ou qu'aucune valeur ne peut être appliqué pour cette ligne (exemple EscapeVelocity pour un trou noir Star).

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

Dans votre cas, vous voulez autoriser NULLs dans votre colonne, mais vous voulez qu'un seul NULL soit autorisé. Pourquoi? Quel genre de relation est-ce entre les deux tables?

Vous pouvez peut-être simplement changer la colonne en NOT NULL et stocke, au lieu de NULL, une valeur spéciale (comme -1) qui est connu pour ne jamais apparaître. Cela résoudra le problème de contrainte d'unicité (mais peut avoir d'autres effets secondaires potentiellement indésirables. Par exemple, en utilisant -1 signifiant "inconnu/ne s'applique pas" faussera tout calcul de somme ou de moyenne dans la colonne. Ou tous ces calculs devront tenir compte de la valeur spéciale et l'ignorer.)

8
ypercubeᵀᴹ