LOXODATA

Contrainte d'exclusion

2019-01-30   742 mots, 4 minutes de lecture   Sébastien Lardière

Cet article commence une série présentant des fonctionnalités du langage SQL disponibles dans PostgreSQL. Vous pouvez retrouver une comparaison de la disponibilité des fonctionnalités ici: http://www.sql-workbench.eu/dbms_comparison.html. Ces fonctionnalités ne sont pas forcément présentes dans la norme SQL.

Ce premier article présente une fonctionnalité très utile lorsqu’on souhaite manipuler des données dont le type est plus complexe qu’un simple scalaire : la contrainte d’exclusion.

Le but de cette fonctionnalité est de vérifier que les données qu’on insère dans une table ne sont pas redondantes, qu’il n’y a pas de doublons.

La contrainte d’unicité

Pour ceci, il existe une fonctionnalité très courante lorsqu’on utilise une base de données relationnelle : la contrainte d’unicité.

Une contrainte d’unicité garantit l’unicité des données contenues dans une colonne ou un groupe de colonnes par rapport à toutes les lignes de la table. Elle n’autorise donc pas l’insertion de doublon dans une table, ce qui permet de s’assurer de la qualité des données.

Par exemple, la table suivante permet de connaitre l’ensemble des plages d’adresses IPs utiles :

CREATE TABLE reseau
(
  id int GENERATED BY DEFAULT AS IDENTITY primary key
  , adresses ip4r
  , commentaire text
);

L’attribut adresses contient donc les adresses des réseaux, avec le type de données ip4r fourni par l’extension du même nom : https://github.com/RhodiumToad/ip4r.

Si on veut éviter d’enregistrer des doublons, il est alors nécessaire d’ajouter une contrainte d’unicité :

ALTER TABLE reseau ADD UNIQUE ( adresses ) ;

Cet ordre crée un index btree d’unicité (on parle aussi d’index unique, mais ce n’est pas l’index qui est unique), mettant en œuvre la contrainte. Il n’est alors plus possible d’ajouter de plage d’adresse IP en doublon :

INSERT INTO reseau (adresses)
  VALUES ('192.168.122.0/24')
       , ('192.168.89.0/24');
INSERT 0 2

INSERT INTO reseau (adresses)
  VALUES ('192.168.122.0/24');
ERROR:  duplicate key value violates unique constraint "reseau_adresses_key"
DETAIL:  Key (adresses)=(192.168.122.0/24) already exists.

Mais si on ajoute un réseau qui interfère avec une entrée existante, sans être strictement égal, que se passe-t-il ?

INSERT INTO reseau (adresses)
  VALUES ('192.168.122.0/28');
INSERT 0 1

On constate que l’entrée est valide, alors qu’il y a potentiellement un problème d’intégrité de nos données : la plage d’adresses IP 192.168.122.0/28 est en réalité comprise dans la plage 192.168.122.0/24, et il y a donc un problème fonctionnel.

La raison est simple : la contrainte d’unicité ne sait utiliser que l’opérateur d'égalité (=). Pour constater le conflit d’adresses réseau, il nous faut utiliser l’opérateur de chevauchement : &&.

La contrainte d’exclusion

On pourrait simplement résumer la contrainte d’exclusion comme étant une extension de la contrainte d’unicité, avec un opérateur de comparaison différent de l'égalité.

L’opérateur de comparaison doit être commutatif, indexable (btree ou gist) et renvoyer un booléen (le type d’index gin n’est pas supporté).

Dans le cas de notre exemple, l’opérateur de comparaison && fourni par l’extension ip4r remplit les conditions, grâce à la classe d’opérateurs gist_ip4r_ops autorisant le parcours d’un index gist.

La création de la table est alors :

CREATE TABLE reseau
(
  id int GENERATED BY DEFAULT AS IDENTITY primary key
  , adresses ip4r
  , commentaire text
);

Et la création de la contrainte d’exclusion est :

ALTER TABLE reseau
  ADD CONSTRAINT reseau_adresses_excl EXCLUDE
    USING gist
    ( adresses WITH && );

Comme pour la contrainte d’unicité, la contrainte crée implicitement un index, ici de type gist, précisé avec le mot-clé USING.

Le mot-clé EXCLUDE permet donc de préciser le type de contrainte, et le mot-clé WITH indique l’opérateur utilisé.

Ensuite, l’insertion de données permet de mettre en évidence le contrôle d’intégrité :

INSERT INTO reseau (adresses)
  VALUES ('192.168.122.0/24'), ('192.168.89.0/24');
INSERT 0 2

INSERT INTO reseau (adresses)
  VALUES ('192.168.122.0/24');
ERROR:  conflicting key value violates
        exclusion constraint "reseau_adresses_excl"
DETAIL:  Key (adresses)=(192.168.122.0/24) conflicts with
         existing key (adresses)=(192.168.122.0/24).

INSERT INTO reseau (adresses)
  VALUES ('192.168.122.0/28');
ERROR:  conflicting key value violates
        exclusion constraint "reseau_adresses_excl"
DETAIL:  Key (adresses)=(192.168.122.0/28) conflicts with
         existing key (adresses)=(192.168.122.0/24).

On comprend aisément en lisant les messages d’erreurs que la contrainte d’exclusion permet de couvrir les fonctionnalités de la contrainte d’unicité, et de mettre un œuvre une contrainte d’intégrité liée à la nature de la donnée (non-scalaire), implémentée par un opérateur spécifique (&&).

Conclusion

La notion de contrainte d’exclusion est un outil puissant pour assurer l’intégrité de données : une donnée non scalaire avec un opérateur autre que la simple égalité est alors stockée en toute sérénité.

Le prochain article de la série présentera une famille de type de données ressemblant à ip4r : les plages de valeurs. Et les contraintes d’exclusion seront à l'œuvre !