Contrainte unique PostgreSQL vs index

157

Comme je peux comprendre la documentation, les définitions suivantes sont équivalentes:

create table foo (
    id serial primary key,
    code integer,
    label text,
    constraint foo_uq unique (code, label));

create table foo (
    id serial primary key,
    code integer,
    label text);
create unique index foo_idx on foo using btree (code, label);    

Cependant, une note dans le manuel de Postgres 9.4 dit:

La méthode préférée pour ajouter une contrainte unique à une table est ALTER TABLE ... ADD CONSTRAINT. L'utilisation d'index pour appliquer des contraintes uniques peut être considérée comme un détail d'implémentation auquel il ne faut pas accéder directement.

(Edit: cette note a été supprimée du manuel avec Postgres 9.5.)

Est-ce seulement une question de style? Quelles sont les conséquences pratiques du choix de l'une de ces variantes (par exemple sur les performances)?

Adam Piotrowski
la source
23
La (seule) différence pratique est que vous pouvez créer une clé étrangère vers une contrainte unique mais pas vers un index unique.
a_horse_with_no_name
29
Un avantage dans l'autre sens ( comme cela a été soulevé dans une autre question récemment ) est que vous pouvez avoir un index unique partiel , tel que "Unique (foo) Where bar Is Null". AFAIK, il n'y a aucun moyen de faire ça avec une contrainte.
IMSoP
3
@a_horse_with_no_name Je ne sais pas quand cela s'est produit, mais cela ne semble plus être vrai. Ce violon SQL autorise les références de clé étrangère à un index unique: sqlfiddle.com/#!17/20ee9 ; EDIT: l'ajout d'un `` filtre '' à l'index unique fait que cela cesse de fonctionner (comme prévu)
user1935361
1
à partir de la documentation postgres: PostgreSQL crée automatiquement un index unique lorsqu'une contrainte unique ou une clé primaire est définie pour une table. postgresql.org/docs/9.4/static/indexes-unique.html
maggu
Je suis d'accord avec @ user1935361, s'il n'était pas possible de créer une clé étrangère vers un index unique (avec PG 10 au moins), j'aurais rencontré ce problème il y a longtemps.
Andy

Réponses:

132

J'avais des doutes sur cette question fondamentale mais importante, alors j'ai décidé d'apprendre par l'exemple.

Créons un maître de table de test avec deux colonnes, con_id avec une contrainte unique et ind_id indexé par un index unique.

create table master (
    con_id integer unique,
    ind_id integer
);
create unique index master_unique_idx on master (ind_id);

    Table "public.master"
 Column |  Type   | Modifiers
--------+---------+-----------
 con_id | integer |
 ind_id | integer |
Indexes:
    "master_con_id_key" UNIQUE CONSTRAINT, btree (con_id)
    "master_unique_idx" UNIQUE, btree (ind_id)

Dans la description de la table (\ d dans psql), vous pouvez distinguer la contrainte unique de l'index unique.

Unicité

Vérifions l'unicité, juste au cas où.

test=# insert into master values (0, 0);
INSERT 0 1
test=# insert into master values (0, 1);
ERROR:  duplicate key value violates unique constraint "master_con_id_key"
DETAIL:  Key (con_id)=(0) already exists.
test=# insert into master values (1, 0);
ERROR:  duplicate key value violates unique constraint "master_unique_idx"
DETAIL:  Key (ind_id)=(0) already exists.
test=#

Cela fonctionne comme prévu!

Clés étrangères

Nous allons maintenant définir une table détaillée avec deux clés étrangères faisant référence à nos deux colonnes dans master .

create table detail (
    con_id integer,
    ind_id integer,
    constraint detail_fk1 foreign key (con_id) references master(con_id),
    constraint detail_fk2 foreign key (ind_id) references master(ind_id)
);

    Table "public.detail"
 Column |  Type   | Modifiers
--------+---------+-----------
 con_id | integer |
 ind_id | integer |
Foreign-key constraints:
    "detail_fk1" FOREIGN KEY (con_id) REFERENCES master(con_id)
    "detail_fk2" FOREIGN KEY (ind_id) REFERENCES master(ind_id)

Eh bien, pas d'erreurs. Assurons-nous que cela fonctionne.

test=# insert into detail values (0, 0);
INSERT 0 1
test=# insert into detail values (1, 0);
ERROR:  insert or update on table "detail" violates foreign key constraint "detail_fk1"
DETAIL:  Key (con_id)=(1) is not present in table "master".
test=# insert into detail values (0, 1);
ERROR:  insert or update on table "detail" violates foreign key constraint "detail_fk2"
DETAIL:  Key (ind_id)=(1) is not present in table "master".
test=#

Les deux colonnes peuvent être référencées dans des clés étrangères.

Contrainte utilisant l'index

Vous pouvez ajouter une contrainte de table à l'aide d'un index unique existant.

alter table master add constraint master_ind_id_key unique using index master_unique_idx;

    Table "public.master"
 Column |  Type   | Modifiers
--------+---------+-----------
 con_id | integer |
 ind_id | integer |
Indexes:
    "master_con_id_key" UNIQUE CONSTRAINT, btree (con_id)
    "master_ind_id_key" UNIQUE CONSTRAINT, btree (ind_id)
Referenced by:
    TABLE "detail" CONSTRAINT "detail_fk1" FOREIGN KEY (con_id) REFERENCES master(con_id)
    TABLE "detail" CONSTRAINT "detail_fk2" FOREIGN KEY (ind_id) REFERENCES master(ind_id)

Il n'y a plus de différence entre la description des contraintes de colonne.

Index partiels

Dans la déclaration de contrainte de table, vous ne pouvez pas créer d'index partiels. Cela vient directement de la définition de create table .... Dans la déclaration d'index unique, vous pouvez définir WHERE clausepour créer un index partiel. Vous pouvez également créer un index sur l'expression (pas seulement sur la colonne) et définir d'autres paramètres (classement, ordre de tri, placement NULL).

Vous ne pouvez pas ajouter de contrainte de table à l'aide d'un index partiel.

alter table master add column part_id integer;
create unique index master_partial_idx on master (part_id) where part_id is not null;

alter table master add constraint master_part_id_key unique using index master_partial_idx;
ERROR:  "master_partial_idx" is a partial index
LINE 1: alter table master add constraint master_part_id_key unique ...
                               ^
DETAIL:  Cannot create a primary key or unique constraint using such an index.
klin
la source
est-ce une information réelle? en particulier sur les index partiels
anatol
1
@anatol - oui, ça l'est.
klin
30

Un autre avantage de l'utilisation de UNIQUE INDEXvs. UNIQUE CONSTRAINTest que vous pouvez facilement DROP/ CREATEun index CONCURRENTLY, alors qu'avec une contrainte, vous ne pouvez pas.

Vadim Zingertal
la source
4
AFAIK il n'est pas possible de supprimer simultanément un index unique. postgresql.org/docs/9.3/static/sql-dropindex.html "Il y a plusieurs mises en garde à prendre en compte lors de l'utilisation de cette option. Un seul nom d'index peut être spécifié, et l'option CASCADE n'est pas prise en charge. (Ainsi, un index qui prend en charge une contrainte UNIQUE ou PRIMARY KEY ne peut pas être supprimée de cette façon.) "
Rafał Cieślak
15

L'unicité est une contrainte. Il se trouve être implémenté via la création d'un index unique puisqu'un index est rapidement capable de rechercher toutes les valeurs existantes afin de déterminer si une valeur donnée existe déjà.

Conceptuellement, l'index est un détail d'implémentation et l'unicité ne doit être associée qu'aux contraintes.

Le texte intégral

Les performances de vitesse devraient donc être les mêmes

Eugen Konkov
la source
4

Une autre chose que j'ai rencontrée est que vous pouvez utiliser des expressions sql dans des index uniques mais pas dans des contraintes.

Donc, cela ne fonctionne pas:

CREATE TABLE users (
    name text,
    UNIQUE (lower(name))
);

mais les travaux suivants.

CREATE TABLE users (
    name text
);
CREATE UNIQUE INDEX uq_name on users (lower(name));
김민준
la source
J'utiliserais l' citextextension.
ceving
@ceving cela dépend du cas d'utilisation. parfois, vous voulez préserver le boîtier tout en garantissant l'unicité insensible à la casse
Sampson Crowley
2

Étant donné que diverses personnes ont fourni les avantages des index uniques par rapport aux contraintes uniques, voici un inconvénient: une contrainte unique peut être différée (vérifiée uniquement à la fin de la transaction), un index unique ne peut pas l'être.

Masklinn
la source
Comment cela peut-il être, étant donné que toutes les contraintes uniques ont un index unique?
Chris
1
Étant donné que les index n'ont pas d'API pour le report, seules les contraintes le font, donc bien que la machinerie de report existe sous le couvercle pour prendre en charge des contraintes uniques, il n'y a aucun moyen de déclarer un index comme reportable ou de le différer.
Masklinn
0

J'ai lu ceci dans la doc:

ADD table_constraint [NOT VALID]

Ce formulaire ajoute une nouvelle contrainte à une table en utilisant la même syntaxe que CREATE TABLE, plus l'option NOT VALID, qui n'est actuellement autorisée que pour les contraintes de clé étrangère. Si la contrainte est marquée NOT VALID, la vérification initiale potentiellement longue pour vérifier que toutes les lignes de la table satisfont à la contrainte est ignorée . La contrainte sera toujours appliquée aux insertions ou mises à jour ultérieures (c'est-à-dire qu'elles échoueront à moins qu'il n'y ait une ligne correspondante dans la table référencée). Mais la base de données ne supposera pas que la contrainte est valable pour toutes les lignes de la table, jusqu'à ce qu'elle soit validée à l'aide de l'option VALIDATE CONSTRAINT.

Je pense donc que c'est ce que vous appelez «unicité partielle» en ajoutant une contrainte.

Et, sur la façon de garantir l'unicité:

L'ajout d'une contrainte unique créera automatiquement un index B-tree unique sur la colonne ou le groupe de colonnes répertorié dans la contrainte. Une restriction d'unicité couvrant uniquement certaines lignes ne peut pas être écrite comme une contrainte unique, mais il est possible d'appliquer une telle restriction en créant un index partiel unique.

Remarque: La méthode préférée pour ajouter une contrainte unique à une table est ALTER TABLE… ADD CONSTRAINT. L'utilisation d'index pour appliquer des contraintes uniques peut être considérée comme un détail d'implémentation auquel il ne faut pas accéder directement. Il faut cependant savoir qu'il n'est pas nécessaire de créer manuellement des index sur des colonnes uniques; cela ne ferait que dupliquer l'index créé automatiquement.

Nous devons donc ajouter une contrainte, qui crée un index, pour garantir l'unicité.

Comment je vois ce problème?

Une «contrainte» vise à s'assurer de manière grammaticale que cette colonne doit être unique, elle établit une loi, une règle; tandis que "index" est sémantique , sur "comment implémenter, comment atteindre l'unicité, ce que signifie unique quand il s'agit de l'implémentation". Donc, la façon dont Postgresql l'implémente est très logique: d' abord, vous déclarez qu'une colonne doit être unique, puis Postgresql ajoute l'implémentation de l'ajout d'un index unique pour vous .

WesternGun
la source
1
"Donc je pense que c'est ce que vous appelez" unicité partielle "en ajoutant une contrainte." les index ne peuvent s'appliquer qu'à un sous-ensemble bien défini d'enregistrements via lewhere clause, vous pouvez donc définir que les enregistrements sont des IFF uniques et qu'ils satisfont à certains critères. Cela désactive simplement les contraintes pour un ensemble non défini d'enregistrements qui sont antérieurs à la création de la contrainte. C'est complètement différent, et ce dernier est nettement moins utile, bien qu'il soit pratique pour les migrations progressives je suppose.
Masklinn
0

Il y a une différence de verrouillage.
L'ajout d'un index ne bloque pas l'accès en lecture à la table.
L'ajout d'une contrainte met un verrou de table (donc toutes les sélections sont bloquées) puisqu'elle est ajoutée via ALTER TABLE .

Bax
la source
0

Une chose très mineure qui peut être faite avec des contraintes uniquement et non avec des index est l'utilisation de la ON CONFLICT ON CONSTRAINTclause ( voir aussi cette question ).

Cela ne fonctionne pas:

CREATE TABLE T (a INT PRIMARY KEY, b INT, c INT);
CREATE UNIQUE INDEX u ON t(b);

INSERT INTO T (a, b, c)
VALUES (1, 2, 3)
ON CONFLICT ON CONSTRAINT u
DO UPDATE SET c = 4
RETURNING *;

Cela produit:

[42704]: ERROR: constraint "u" for table "t" does not exist

Transformez l'index en contrainte:

DROP INDEX u;
ALTER TABLE t ADD CONSTRAINT u UNIQUE (b);

Et la INSERTdéclaration fonctionne maintenant.

Lukas Eder
la source