Comment valider les données : le guide complet

Les six contrôles qui détectent la plupart des incidents réels, où les exécuter, et comment transformer du SQL ponctuel en un contrat de données versionné qui s'exécute selon une planification.

· 13 min read

Pour valider des données, écrivez chaque attente sous la forme d'une requête qui renvoie le nombre de lignes en violation, exécutez l'ensemble de ces requêtes sur le jeu de données après chaque chargement, et déclarez l'échec dès qu'un compteur franchit un seuil convenu à l'avance. Six contrôles détectent l'immense majorité des incidents réels : valeurs manquantes dans les colonnes obligatoires, clés métier en doublon, valeurs hors d'un ensemble autorisé, nombres hors d'une plage plausible, chaînes mal formées et lignes périmées. Les contrôles eux-mêmes ne sont que du SQL ordinaire — la difficulté consiste à les exécuter là où vivent les données, à les maintenir alignés sur le schéma, et à alerter d'une manière que les gens n'apprennent pas à ignorer. Tout ce qui suit décrit la mécanique pour y parvenir de façon fiable.

Ce qu'est réellement la validation des données

Trois choses portent le nom de validation, et une seule fait l'objet de ce guide.

La validation des entrées intervient à la lisière d'une application : un formulaire rejette une adresse e-mail dépourvue d'@. C'est un filtre à l'écriture, enregistrement par enregistrement.

La validation de schéma vérifie la structure : la table possède-t-elle les colonnes attendues par le chargeur, dans les types attendus ? Elle détecte une rupture de contrat entre systèmes, pas de mauvaises valeurs à l'intérieur d'une structure bien formée.

La validation des données contrôle le contenu d'un jeu de données qui existe déjà, en masse, après son atterrissage : sur les 4,2 millions de lignes chargées cette nuit, combien rompent une promesse sur laquelle quelqu'un s'appuie ?

Cette distinction dicte la forme de la solution. Vous ne rejetez pas de lignes ; elles sont déjà là. Vous mesurez, selon une planification, et vous décidez que faire de la mesure. Une exécution de validation produit un nombre par règle, un pass ou un fail par règle, et un historique — c'est ce qui transforme « les données ont l'air bizarres » en « le taux de valeurs nulles sur customer_id est passé de 0,02 % à 11 % mardi dernier à 04:12 ». Si vous voulez la définition pour elle-même, y compris ce qui distingue la validation des tests, de l'observabilité et du nettoyage, commencez par ce qu'est la validation des données et revenez ici pour la mécanique.

Les six contrôles dont chaque jeu de données a besoin

Tous les contrôles ont la même forme : comptez les lignes qui violent l'attente, comparez ce nombre à un seuil.

SELECT count(*) AS violations
FROM sales.orders
WHERE order_id IS NULL;

Le schéma tient tout entier là-dedans. Une règle, c'est un prédicat, un comptage et un seuil ; et les six qui comptent ne sont que six prédicats.

ContrôleDimensionCe qu'il détectePrédicat
ComplétudecompletenessUne colonne qui a cessé d'être alimentéecol IS NULL
UnicitéuniquenessRéexécutions, chargements relancés, chiffre d'affaires compté deux foisGROUP BY key HAVING count(*) > 1
ValiditéconformityUne nouvelle valeur d'énumération dont personne ne vous a parlécol NOT IN ('a', 'b', 'c')
PlageaccuracyErreurs d'unité, erreurs de devise, quantités négativescol < 0 OR col > 100000
FormatconformityIdentifiants mal formés, codes tronquéscol NOT LIKE / !~ pattern
FraîcheurtimelinessUn pipeline qui s'est arrêté en silencemax_ts < now() - interval

Deux ajouts méritent leur place sur la plupart des tables : une règle rowCount au niveau de la table, parce qu'un résultat vide après un chargement est un mode de défaillance distinct et très courant ; et un contrôle d'intégrité référentielle sur les clés étrangères, parce que les rechargements partiels laissent des orphelins que les jointures internes écartent sans bruit.

Le piège des valeurs nulles, que tout le monde rencontre une fois

Écrivez le contrôle de validité tel qu'il apparaît ci-dessus et il vous mentira :

-- Wrong: nulls disappear from the count.
WHERE status NOT IN ('pending', 'paid', 'shipped', 'refunded')

-- Right:
WHERE status IS NOT NULL
  AND status NOT IN ('pending', 'paid', 'shipped', 'refunded')

NULL NOT IN (...) s'évalue à NULL, et non à true, dans tous les moteurs SQL. Sans la garde, une colonne nulle à 90 % affiche une conformité parfaite. C'est de loin la cause la plus fréquente de contrôle qui passe à tort dans du SQL de qualité des données écrit à la main, et cela vaut la peine de vérifier ce point sur chaque règle dont vous héritez.

Où exécuter les contrôles

Exécutez-les dans l'entrepôt, via une connexion en lecture seule, et ne déplacez jamais les lignes.

Extraire les données pour les valider est le mauvais réflexe, pour trois raisons : c'est lent, cela recopie des lignes sensibles dans un second système qui exigera une seconde revue de sécurité, et cela ne tient pas la cadence — un contrôle qui doit exporter une table de faits d'un milliard de lignes ne s'exécutera pas toutes les heures. Pousser l'agrégation vers le moteur signifie que chaque contrôle devient un simple SELECT renvoyant un seul nombre : exactement la charge de travail pour laquelle tout entrepôt est conçu.

Le jeu de permissions est réellement minimal : se connecter à la base, lire le catalogue de schéma, SELECT sur les tables contrôlées. Rien d'autre. Si un outil de validation réclame un accès en écriture, demandez pourquoi. Pointez la connexion vers un réplica de lecture lorsqu'il en existe un — ce sont des balayages d'agrégation sans autre exigence de cohérence que « récent » — et fixez un délai d'expiration de requête pour qu'un balayage non indexé ne puisse pas monopoliser une connexion pendant une heure.

SQL manuel, framework ou service géré

Il y a trois façons honnêtes de procéder, et la bonne dépend du nombre de jeux de données que vous avez et de qui doit pouvoir lire les règles.

ApprocheSes points fortsLà où elle craque
SQL écrit à la main dans un cronAucune mise en place, contrôle total, pas de nouveau fournisseurLes règles dérivent du schéma ; personne ne sait quels scripts tournent encore
Un framework (tests dbt, Great Expectations, Soda)Versionné, s'exécute dans votre pipelineLes règles sont enfermées dans le format de ce moteur ; ne couvre que ce que l'outil possède
Un service de validation géréPlanification, historique, alertes et une interface lisible par des non-ingénieursVous confiez une connexion à un fournisseur, et le format des règles est généralement le sien

Le mode de défaillance de la première approche, c'est l'entropie : en un an, vous avez quarante fichiers SQL, dont six référencent des colonnes supprimées, et personne n'ose en effacer un seul. Celui de la deuxième, c'est la portée — les tests dbt sont excellents, mais ils ne couvrent que les modèles construits par dbt, et ils s'exécutent quand dbt s'exécute. Celui de la troisième, c'est l'enfermement propriétaire, et c'est la raison d'être de la section suivante.

Aucune de ces approches n'est exclusive. Les équipes qui réussissent placent des assertions rapides et peu coûteuses dans le pipeline, là où elles font échouer un build, et conservent les promesses durables dans un endroit où elles sont relues et surveillées, quel que soit l'outil qui a écrit la table aujourd'hui.

Les contrats de données : versionner les attentes, pas les scripts

Le remède durable à l'entropie des règles consiste à cesser d'écrire les contrôles comme du code pour les écrire comme un document — une déclaration de ce que le jeu de données promet, stockée à côté du reste de vos sources, relue en pull request, et exécutée par le moteur que vous utilisez. Ce document, c'est un contrat de données.

L'Open Data Contract Standard est la spécification ouverte qui permet d'en écrire un. C'est du YAML, il est développé au sein du projet Bitol de la Linux Foundation plutôt que par un éditeur, et un contrat minimal couvrant les contrôles ci-dessus ressemble à ceci :

apiVersion: v3.0.0
kind: DataContract
info:
  title: orders
  version: 1.0.0
  owner: data-platform
schema:
  - name: orders
    physicalName: orders
    physicalType: table
    properties:
      - name: order_id
        logicalType: string
        required: true
        primaryKey: true
        quality:
          - rule: nullCount
            dimension: completeness
            severity: error
            mustBe: "0"
          - rule: duplicateCount
            dimension: uniqueness
            severity: error
            mustBe: "0"
      - name: status
        logicalType: string
        quality:
          - rule: validValues
            dimension: conformity
            severity: error
            mustBe: "['pending', 'paid', 'shipped', 'refunded']"
      - name: total_amount
        logicalType: number
        quality:
          - rule: between
            dimension: accuracy
            severity: error
            mustBe: "[0, 100000]"
      - name: created_at
        logicalType: timestamp
        quality:
          - rule: freshness
            dimension: timeliness
            severity: error
            mustBe: "<= 24h"
    quality:
      - rule: rowCount
        dimension: completeness
        severity: warning
        mustBe: "> 0"

Trois propriétés justifient l'effort.

C'est portable. nullCount veut dire la même chose partout ; le moteur d'exécution le compile vers le dialecte de chaque base. Migrer d'entrepôt cesse d'être une migration de règles.

C'est relisible. Un changement de seuil devient un diff avec un auteur et une raison. Savoir si la borne a bougé parce que le métier a changé ou parce que quelqu'un en avait assez de l'alerte redevient traçable — ce qui n'est jamais le cas dans l'interface d'un éditeur.

C'est à vous. Des règles écrites dans un standard ouvert ne sont pas un actif de celui qui les exécute aujourd'hui.

Catalyst est bâti directement sur ODCS : le YAML est le format de stockage, et non le rendu d'une ligne de base de données. Une règle modifiée dans l'éditeur visuel produit donc un diff d'une ligne, et le contrat relu en pull request est, octet pour octet, celui qui s'exécute.

Planification et alertes

Deux règles, apprises l'une comme l'autre à la dure.

Déclenchez depuis le chargement, pas depuis une horloge. Une table quotidienne validée à 06:00 alors que l'ELT se termine à 06:40 échouera tous les matins pour une raison qui n'a rien à voir avec la qualité. Alignez la cadence sur celle des données : fraîcheur horaire sur une table en flux, une exécution après le batch nocturne pour tout le reste. Lancer les contrôles plus souvent que les données ne changent produit du bruit et, sur les entrepôts facturés à l'usage, une facture.

Alertez sur les transitions, pas sur les états. « Orders vient de passer en échec » est actionnable. « Orders est toujours en échec », répété toutes les heures, c'est ainsi qu'un canal finit en sourdine — et un canal en sourdine est pire qu'une absence d'alertes, parce qu'il a l'apparence d'une couverture.

Servez-vous de la sévérité pour séparer les deux populations de règles. Réservez error aux contrôles qui doivent réellement bloquer un consommateur en aval : un rafraîchissement de tableau de bord, une synchronisation reverse-ETL, un rapport financier. Tout le reste — contrôles de motif sur des champs saisis par des humains, dérive du nombre de lignes, anomalies de distribution — relève de warning, où il devient une courbe de tendance plutôt qu'une astreinte.

Quels jeux de données valider en premier

Vous ne pouvez pas tout valider, et essayer de le faire est le meilleur moyen de tuer le projet. Classez par rayon d'impact :

  1. Tout ce qui alimente un chiffre lu par un dirigeant. Chiffre d'affaires, effectifs, pipeline commercial. Un chiffre faux ici coûte une crédibilité qui met des mois à se reconstruire.
  2. Tout ce qui alimente une décision automatisée. Tarification, plafonds de crédit, features de machine learning, reverse-ETL vers un CRM. Ces systèmes agissent sur de mauvaises données avant qu'un humain ne les voie.
  3. Tout ce qui a un consommateur externe. Un flux partenaire ou une déclaration réglementaire a un coût de défaillance que vous ne maîtrisez pas.
  4. Les jointures au cœur de votre modèle. Les tables de dimensions auxquelles tout se rattache, où une clé orpheline retire discrètement des lignes de chaque requête en aval.

Commencez par cinq à dix règles sur une seule de ces tables plutôt que par trois règles sur quarante. Une couverture superficielle partout ne vous apprend rien ; une couverture profonde sur les tables qui comptent attrape les incidents dont vous entendriez sinon parler par quelqu'un d'autre.

Lisez ensuite le guide correspondant à votre moteur — les six contrôles sont universels, mais le SQL, les pièges et le modèle de coût ne le sont pas :

Catalyst met en œuvre exactement le flux décrit ci-dessus — importer le schéma, proposer un contrat de référence, compiler chaque règle vers le SQL de votre moteur, l'exécuter selon une planification et suivre l'historique pass / warn / fail par règle — sur l'ensemble de ces connexions, avec une offre gratuite pour l'essayer sur un jeu de données (PostgreSQL et MySQL) avant de décider quoi que ce soit (tarifs).

Questions fréquentes

Qu'est-ce qui constitue un contrôle de validation des données ?

Un contrôle de validation des données est une attente convenue au sujet du contenu d'un jeu de données, exprimée sous la forme d'une requête qui renvoie le nombre de lignes en violation : aucune valeur manquante dans une colonne obligatoire, aucune clé en doublon, des valeurs comprises dans un ensemble autorisé ou dans une plage plausible, des chaînes correctement formatées, ou des lignes assez récentes pour être utiles. Le contrôle échoue lorsque ce nombre franchit un seuil défini à l'avance. Contrairement à la validation des entrées, il s'exécute en masse sur des données déjà atterries.

Quels sont les principaux types de contrôles de validation des données ?

Six couvrent l'essentiel des incidents réels : la complétude (valeurs nulles dans les colonnes obligatoires), l'unicité (clés métier en doublon), la validité (valeurs hors d'un ensemble autorisé), l'exactitude (nombres hors d'une plage plausible), la conformité (chaînes qui ne respectent pas un format requis) et l'actualité (lignes ou partitions périmées). Le nombre de lignes au niveau de la table et l'intégrité référentielle entre tables sont les deux compléments les plus courants.

Dois-je écrire du SQL pour valider des données ?

Pas pour les contrôles standard. Déclarer l'attente — obligatoire, unique, valeurs autorisées, plage numérique, motif, âge maximal, clé étrangère — permet à un moteur d'exécution de la compiler vers le SQL correct pour votre base, ce qui élimine au passage les différences de dialecte et les pièges liés aux gardes sur les valeurs nulles. Il vaut mieux réserver le SQL écrit à la main à la logique métier véritablement sur mesure, via une règle SQL personnalisée.

À quelle fréquence exécuter les validations de données ?

Alignez la planification sur la cadence propre des données et déclenchez-la depuis le traitement qui les produit, et non depuis une horloge fixe. Les tables en flux appellent des contrôles de fraîcheur horaires ; les tables en batch n'en demandent qu'un, après la fin du chargement. Valider plus souvent que les données ne changent ne produit que du bruit — et, sur les entrepôts facturés à l'usage, une facture.

La validation des données et un contrat de données, est-ce la même chose ?

Non. La validation est l'acte d'exécuter des contrôles ; un contrat de données est le document versionné qui dit quels contrôles doivent exister. Le contrat porte aussi la propriété, les descriptions et les niveaux de service, ce qui le rend relisible par les consommateurs des données et pas seulement par l'équipe qui a écrit le pipeline. Un standard comme ODCS est ce qui garde le contrat portable d'un moteur d'exécution à l'autre.

La validation des données a-t-elle besoin d'un accès en écriture à ma base ?

Non. Chaque contrôle décrit ici est un SELECT qui renvoie un seul nombre : un accès en lecture sur les tables, assorti de la possibilité de lire le catalogue de schéma, constitue l'intégralité des permissions nécessaires. Lorsqu'un réplica de lecture ou un secondaire lisible existe, pointez la connexion vers lui — les contrôles n'ont pas d'autre exigence de cohérence que « récent », et les sortir de l'instance primaire lève la principale objection opérationnelle à les exécuter souvent.