PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Comprendre les contraintes de vérification dans PostgreSQL

La gestion des données est un grand défi. Alors que notre monde tourne, les données continuent d'être répandues, abondantes et intensives. Par conséquent, nous devons prendre des mesures pour gérer l'afflux.

Valider chaque élément de données 'à la main ' 24 heures sur 24 est tout simplement impossible. Quel rêve fantastique. Mais, après tout, ce n'est que cela. Un rêve. De mauvaises données sont de mauvaises données. Peu importe comment vous le tranchez ou le coupez en dés (jeu de mots). C'est un problème dès le début, ce qui entraîne encore plus de problèmes.

Les bases de données modernes gèrent une grande partie du travail lourd pour nous. Beaucoup fournissent des solutions intégrées pour aider à gérer ce domaine particulier de données.

Un moyen sûr de contrôler les données saisies dans la colonne d'une table consiste à utiliser un type de données. Besoin d'une colonne avec des nombres décimaux, ayant un nombre total de chiffres de 4, avec 2 d'entre eux après la virgule ?

Chose sûre! Aucun problème.

NUMERIC(4,2), une option viable, garde cette colonne comme un chien de garde. Les valeurs de texte de caractère peuvent-elles s'y glisser ? Pas une chance de faire boule de neige.

PostgreSQL propose une multitude de types de données. Il y a de fortes chances qu'il en existe déjà un pour répondre à vos besoins. Sinon, vous pouvez créer le vôtre. (Voir :PostgreSQL CREATE TYPE)

Pourtant, les types de données seuls ne suffisent pas. Vous ne pouvez pas garantir que les exigences les plus spécifiques sont couvertes et conformes à une structure aussi large. Des règles de conformité et une sorte de "norme" sont généralement requises lors de la conception d'un schéma.

Supposons que dans cette même colonne NUMERIC(4,2), vous ne vouliez que des valeurs supérieures à 25,25 mais inférieures à 74,33 ? Si la valeur 88.22 est mémorisée, le type de données n'est pas en cause. En autorisant 4 chiffres au total, avec 2 au plus après la virgule, il fait son travail. Rejetez la faute ailleurs.

Comment gagnons-nous sur ce front lorsqu'il s'agit de contrôler les données autorisées dans notre base de données ? La cohérence des données est une priorité absolue et fait partie intégrante de toute solution de données solide. Au cas où vous auriez contrôlé les données collectées depuis le début de leur source d'origine, la cohérence serait probablement moins un problème.

Mais, un monde parfait n'existe (peut-être) que dans l'un de ces nombreux romans fantastiques que j'aime lire.

Malheureusement, les données incomplètes, incohérentes et "sales" sont des caractéristiques et des réalités trop courantes présentes dans un domaine centré sur la base de données.

Cependant, tout n'est pas perdu dans le malheur car nous avons des contraintes de vérification pour atténuer ces problèmes. Pour ces règles spécifiques, nous devons mettre en place, par nécessité, qui garantit que nous ne traitons et ne stockons que des données cohérentes. En imposant ces spécifications dans la base de données, nous pouvons minimiser l'impact des données incohérentes sur nos objectifs commerciaux et nos solutions à venir.

Qu'est-ce qu'une contrainte ? - Une définition de haut niveau

Dans ce contexte, une contrainte est un type de règle ou de restriction placée sur une colonne de table de base de données. Cette spécificité exige que les données entrantes soient conformes aux exigences définies avant d'être stockées. Lesdites exigences ont tendance à être "professionnellement" inventées (et le sont souvent) en tant que règles commerciales . Cela se résume à un test booléen de validation pour la vérité. Si les données passent (true), elles sont stockées. Sinon, pas de saisie (faux).

Contraintes disponibles dans PostgreSQL

Au moment de la rédaction, la documentation de PostgreSQL répertorie 6 catégories de contraintes.

Ce sont :

  • Vérifier les contraintes
  • Contraintes non nulles
  • Contraintes uniques
  • Clés primaires
  • Clés étrangères
  • Contraintes d'exclusion

Vérifier les contraintes

Un exemple simple pour une colonne INTEGER serait d'interdire les valeurs supérieures à, disons, 100.

learning=> CREATE TABLE no_go(id INTEGER CHECK (id < 100));
CREATE TABLE
learning=> INSERT INTO no_go(id) VALUES(101);
ERROR: new row for relation "no_go" violates check constraint "no_go_id_check"
DETAIL: Failing row contains (101).

Comme vu ci-dessus, les tentatives d'INSERER des valeurs qui violent la contrainte de vérification échouent.

Les contraintes de vérification ne surveillent pas seulement les colonnes pendant INSERT, même les instructions UPDATE (et d'autres, par exemple, \copy et COPY) doivent également respecter les restrictions.

Supposons que la table no_go ait cette valeur :

learning=> TABLE no_go;
id 
----
55
(1 row)

Un UPDATE sur la valeur de la colonne id vers une valeur qui n'est pas conforme à la contrainte Check échoue également :

learning=> UPDATE no_go SET id = 155
learning-> WHERE id = 55;
ERROR: new row for relation "no_go" violates check constraint "no_go_id_check"
DETAIL: Failing row contains (155).

Les contraintes de vérification doivent "avoir un sens" pour le type de données de la colonne cible. Il n'est pas valide de tenter et de contraindre une colonne INTEGER à interdire le stockage de valeurs de texte puisque le type de données lui-même ne le permettra pas.

Voir cet exemple où j'essaie d'imposer ce type de contrainte Check lors de la création de la table :

learning=> CREATE TABLE num_try(id INTEGER CHECK(id IN ('Bubble', 'YoYo', 'Jack-In-The-Box')));
ERROR: invalid input syntax for integer: "Bubble"

La vie sans contraintes de contrôle

Un vieil adage que j'ai entendu et qui résonne en moi est :"L'eau ne vous manque pas tant que le puits n'est pas à sec . "

Sans les contraintes de Check, nous pouvons sûrement comprendre que leur avantage remarquable est le plus apprécié lorsque vous devez vous en passer.

Prenons cet exemple…

Pour commencer, nous avons ce tableau et des données qui représentent les matériaux de surface des sentiers :

learning=> SELECT * FROM surface_material;
surface_id | material 
------------+--------------
101 | Gravel
202 | Grass
303 | Dirt
404 | Turf
505 | Concrete
606 | Asphalt
707 | Clay
808 | Polyurethane
(8 rows)

Et cette table avec les noms des sentiers et son propre surface_id :

learning=> SELECT * FROM trails;
id | name | surface_id 
----+-----------------+------------
1 | Dusty Storm | 303
2 | Runners Trip | 808
3 | Pea Gravel Pass | 101
4 | Back 40 Loop | 404
(4 rows)

Nous voulons nous assurer que les pistes de table ne contiennent que des surface_id pour les valeurs correspondantes dans la table surface_material.

Oui oui je sais. Tu me cries dessus.

"Cela ne peut-il pas être réglé avec un CLÉ ÉTRANGÈRE ?!?"

Oui il peut. Mais je l'utilise pour démontrer une utilisation générique, accompagnée d'un écueil à connaître (mentionné plus loin dans le post).

Sans contraintes de vérification, vous pouvez recourir à un TRIGGER et empêcher le stockage de valeurs incohérentes.

Voici un exemple grossier (mais fonctionnel) :

CREATE OR REPLACE FUNCTION check_me()
RETURNS TRIGGER AS
$$
BEGIN
IF NEW.surface_id NOT IN (SELECT surface_id FROM surface_material)
THEN Raise Exception '% is not allowed for surface id', NEW.surface_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE PLpgSQL;
CREATE TRIGGER check_me BEFORE INSERT OR UPDATE ON trails
FOR EACH ROW EXECUTE PROCEDURE check_me();

Les tentatives d'INSERER une valeur qui n'a pas de surface_id correspondant dans les pistes de table échouent :

learning=> INSERT INTO trails(name, surface_id)
learning-> VALUES ('Tennis Walker', 110);
ERROR: 110 is not allowed for surface id
CONTEXT: PL/pgSQL function check_me() line 4 at RAISE

Les résultats de la requête ci-dessous confirment le 'offensant ' la valeur n'a pas été stockée :

learning=> SELECT * FROM trails;
id | name | surface_id 
----+-----------------+------------
1 | Dusty Storm | 303
2 | Runners Trip | 808
3 | Pea Gravel Pass | 101
4 | Back 40 Loop | 404
(4 rows)

C'est certainement beaucoup de travail pour interdire les valeurs indésirables.

Réimplémentons cette exigence avec une contrainte Check.

Comme vous ne pouvez pas utiliser de sous-requête (voici pourquoi j'ai utilisé l'exemple ci-dessus) dans la définition réelle de la contrainte Check, les valeurs doivent être codées en dur .

Pour un petit tableau, ou un exemple trivial tel que présenté ici, c'est très bien. Dans d'autres scénarios, incorporant plus de valeurs, vous serez peut-être mieux servi pour rechercher une solution alternative.

learning=> ALTER TABLE trails ADD CONSTRAINT t_check CHECK (surface_id IN (101, 202, 303, 404, 505, 606, 707, 808));
ALTER TABLE

Ici, j'ai nommé la contrainte de vérification t_check plutôt que de laisser le système la nommer.

(Remarque :Les éléments précédemment définis check_me() FONCTION et accompagnement TRIGGER ont été supprimés (non affichés) avant l'exécution de ce qui suit INSÉRER.)

learning=> INSERT INTO trails(name, surface_id)
VALUES('Tennis Walker', 110);
ERROR: new row for relation "trails" violates check constraint "t_check"
DETAIL: Failing row contains (7, Tennis Walker, 110).

Voudriez-vous regarder comme c'était facile ! Aucun TRIGGER et FUNCTION nécessaires.

Les contraintes de vérification rendent ce type de travail facile.

Vous voulez être astucieux dans la définition de la contrainte de vérification ?

Vous pouvez.

Supposons que vous ayez besoin d'un tableau répertoriant les sentiers un peu plus doux pour ceux qui ont les chevilles et les genoux sensibles. Aucune surface dure souhaitée ici.

Vous voulez vous assurer que tout sentier de randonnée ou piste répertorié dans le tableau nice_trail a un matériau de surface de 'Gravel' ou 'Dirt'.

Cette contrainte de vérification gère cette exigence sans problème :

learning=> CREATE TABLE nice_trail(id SERIAL PRIMARY KEY,
learning(> name TEXT, mat_surface_id INTEGER CONSTRAINT better_surface CHECK(id IN (101, 303))); 
CREATE TABLE

Cela fonctionne très bien.

Mais qu'en est-il d'une FUNCTION qui renvoie les deux identifiants requis pour que la vérification fonctionne ? Une FUNCTION est-elle autorisée dans la définition de contrainte de vérification ?

Oui, on peut être incorporé.

Voici un exemple de travail.

Tout d'abord, le corps de la fonction et sa définition :

CREATE OR REPLACE FUNCTION easy_hike(id INTEGER)
RETURNS BOOLEAN AS
$$
BEGIN
IF id IN (SELECT surface_id FROM surface_material WHERE material IN ('Gravel', 'Dirt'))
THEN RETURN true;
ELSE RETURN false;
END IF;
END;
$$ LANGUAGE PLpgSQL;

Remarquez dans cette instruction CREATE TABLE, je définis la contrainte Check à la 'table ' alors qu'auparavant je n'avais fourni des exemples qu'au niveau de la 'colonne ' niveau.

Les contraintes de vérification définies au niveau de la table sont parfaitement valides :

learning=> CREATE TABLE nice_trail(nt_id SERIAL PRIMARY KEY,
learning(> name TEXT, mat_surface_id INTEGER,
learning(> CONSTRAINT better_surface_check CHECK(easy_hike(mat_surface_id)));
CREATE TABLE

Ces encarts sont tous bons :

learning=> INSERT INTO nice_trail(name, mat_surface_id)
learning-> VALUES ('Smooth Rock Loop', 101), ('High Water Bluff', 303);
INSERT 0 2

Vient maintenant un INSERT pour un sentier qui ne respecte pas la restriction sur la colonne mat_surface_id :

learning=> INSERT INTO nice_trail(name, mat_surface_id)
learning-> VALUES('South Branch Fork', 404);
ERROR: new row for relation "nice_trail" violates check constraint "better_surface_check"
DETAIL: Failing row contains (3, South Branch Fork, 404).

Notre appel FUNCTION dans la définition de la contrainte Check fonctionne comme prévu, limitant les valeurs de colonne indésirables.

Fumée et miroirs ?

Est-ce que tout est comme il semble avec les contraintes Check ? Tout noir et blanc ? Pas de façade à l'avant ?

Un exemple à noter.

Nous avons une table simple dans laquelle nous voulons que la valeur DEFAULT soit 10 pour la seule colonne INTEGER présente :

learning=> CREATE TABLE surprise(id INTEGER DEFAULT 10, CHECK (id <> 10));
CREATE TABLE

Mais, j'ai également inclus une contrainte Check qui interdit une valeur de 10, en définissant id ne peut pas être égal à ce nombre.

Lequel va gagner la journée ? La contrainte DEFAULT ou Check ?

Vous pourriez être surpris de savoir de quoi il s'agit.

J'étais.

Un INSERT arbitraire, fonctionnant correctement :

learning=> INSERT INTO surprise(id) VALUES(17);
INSERT 0 1
learning=> SELECT * FROM surprise;
id 
----
17
(1 row)

Et un INSERT avec la valeur DEFAULT :

learning=> INSERT INTO surprise(id) VALUES(DEFAULT);
ERROR: new row for relation "surprise" violates check constraint "surpise_id_check"
DETAIL: Failing row contains (10).

Oups...

Encore une fois, avec une syntaxe alternative :

learning=> INSERT INTO surprise DEFAULT VALUES;
ERROR: new row for relation "surprise" violates check constraint "surpise_id_check"
DETAIL: Failing row contains (10).

La contrainte Check l'emporte sur la valeur DEFAULT.

Exemple bizarre

La contrainte Check peut apparaître à peu près n'importe où dans la définition de la table lors de la création. Même au niveau de la colonne, il peut être défini sur une colonne non impliquée dans la vérification.

Voici un exemple pour illustrer :

learning=> CREATE TABLE mystery(id_1 INTEGER CHECK(id_2 > id_3),
learning(> id_2 INTEGER, id_3 INTEGER);
CREATE TABLE

Un INSERT pour tester la contrainte :

learning=> INSERT INTO mystery(id_1, id_2, id_3) VALUES (1, 2, 3);
ERROR: new row for relation "mystery" violates check constraint "mystery_check"
DETAIL: Failing row contains (1, 2, 3).

Fonctionne comme prévu.

VALIDATION et NON VALIDE

Nous avons ce tableau et ces données simples :

learning=> CREATE TABLE v_check(id INTEGER);
CREATE TABLE
learning=> INSERT INTO v_check SELECT * FROM generate_series(1, 425);
INSERT 0 425

Supposons que nous devions maintenant implémenter une contrainte Check qui interdit toute valeur inférieure à 50.

Imaginez qu'il s'agit d'une grande table en production et que nous ne pouvons pas vraiment nous permettre d'acquérir un verrou pour le moment, résultant d'une instruction ALTER TABLE. Mais, il faut mettre cette contrainte en place pour aller de l'avant.

ALTER TABLE acquerra un verrou (en fonction de chaque sous-formulaire différent). Comme mentionné, cette table est en production, nous souhaitons donc attendre d'être hors des 'heures de pointe '.

Vous pouvez utiliser l'option NO VALID lors de la création de la contrainte Check :

learning=> ALTER TABLE v_check ADD CONSTRAINT fifty_chk CHECK(id > 50) NOT VALID; 
ALTER TABLE
Téléchargez le livre blanc aujourd'hui PostgreSQL Management &Automation with ClusterControlDécouvrez ce que vous devez savoir pour déployer, surveiller, gérer et faire évoluer PostgreSQLTélécharger le livre blanc

Poursuite des opérations, si une tentative d'INSERT ou de UPDATE viole la contrainte Check :

learning=> INSERT INTO v_check(id) VALUES(22);
ERROR: new row for relation "v_check" violates check constraint "fifty_chk"
DETAIL: Failing row contains (22).

La valeur de la colonne "offenseuse" est interdite.

Ensuite, pendant les temps d'arrêt, nous validons la contrainte Check pour l'appliquer à (toutes) les colonnes préexistantes susceptibles d'être en violation :

learning=> ALTER TABLE v_check VALIDATE CONSTRAINT fifty_chk;
ERROR: check constraint "fifty_chk" is violated by some row

Le message est assez cryptique à mon avis. Mais, il nous informe qu'il y a des lignes non conformes à la contrainte.

Voici quelques points clés que je voulais inclure à partir de la documentation ALTER TABLE (Verbiage directement à partir de la documentation entre guillemets) :

  • Syntaxe :ADD table_constraint [ NOT VALID ] - Description d'accompagnement (partielle) "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 la clé étrangère et CHECK contraintes. 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."
  • Syntaxe :VALIDATE CONSTRAINT nom_contrainte - Description associée (partielle) "Ce formulaire valide une clé étrangère ou une contrainte de vérification qui a été précédemment créée comme NON VALIDE, en parcourant la table pour s'assurer qu'il n'y a pas de lignes pour lesquelles la contrainte n'est pas satisfaite. " "La validation n'acquiert qu'un verrou SHARE UPDATE EXCLUSIVE sur la table en cours de modification."

En aparté, deux points à noter que j'ai appris en cours de route. Les fonctions et les sous-requêtes renvoyant des ensembles ne sont pas autorisées dans les définitions de contraintes de vérification. Je suis sûr qu'il y en a d'autres et j'apprécie tout commentaire à leur sujet dans les commentaires ci-dessous.

Les contraintes de vérification sont impressionnantes. L'utilisation des solutions "intégrées" fournies par la base de données PostgreSQL elle-même, pour appliquer toute restriction de données est parfaitement logique. Le temps et les efforts consacrés à la mise en œuvre des contraintes de vérification pour les colonnes nécessaires dépassent de loin l'absence de mise en œuvre du tout. Gain de temps donc sur le long terme. Plus nous nous appuyons sur la base de données pour gérer ce genre d'exigences, mieux c'est. Nous permettant de concentrer et d'appliquer nos ressources à d'autres domaines/aspects de la gestion de bases de données.

Merci d'avoir lu.