Les références, codes et identifiants importés sont souvent utilisés comme clés de jointure, critères de recherche ou index de tableaux. Leur affichage ne permet pas de vérifier leur contenu réel : une tabulation, un retour chariot ou un espace insécable peut précéder ou suivre une valeur sans être visible dans une interface, un tableur ou une sortie texte courante.

Une clé non normalisée produit des écarts entre données visuellement identiques. Ces écarts affectent les recherches SQL, l'appariement entre sources, les contrôles d'unicité et les conditions applicatives. La normalisation doit donc être contrôlée à l'entrée, dans les données existantes et sur tous les canaux d'écriture.

Limites de TRIM() dans MySQL et MariaDB

Dans MySQL et MariaDB, TRIM() retire l'espace ordinaire, soit l'octet 0x20. La fonction ne retire pas la tabulation (0x09), le retour chariot (0x0D) ni le saut de ligne (0x0A). Une requête de nettoyage fondée uniquement sur TRIM() ne détecte donc pas nécessairement tous les caractères d'espacement placés aux extrémités d'une clé.

UPDATE produits SET ref = TRIM(ref) WHERE ref <> TRIM(ref);

Si cette requête indique qu'aucune ligne n'est affectée, elle établit seulement qu'aucune valeur ne commence ou ne termine par un espace 0x20. Elle ne prouve pas que la colonne est normalisée. Une valeur préfixée par une tabulation peut également satisfaire une comparaison telle que ref = TRIM(ref), puisque la tabulation n'est pas retirée.

Cette distinction est importante pour les colonnes servant de clé. Une valeur contenant un caractère de contrôle reste différente de la même suite de caractères sans ce préfixe ou suffixe. Les comparaisons, index et recherches sont alors réalisés sur des chaînes distinctes.

Contrôler les octets avec HEX()

HEX() fournit une représentation hexadécimale de la valeur stockée. C'est la méthode de contrôle fiable pour toute colonne utilisée comme référence, code, identifiant, clé de rapprochement ou clé de tableau. Elle permet de vérifier les octets sans dépendre du rendu de l'interface.

SELECT id, ref, HEX(ref) FROM produits WHERE id = 1042;
-- ref    : ART0451
-- HEX    : 0941525430343531  ->  le 09 en tête est une tabulation

Dans cet exemple, 09 est le premier octet de la valeur et identifie une tabulation. Les valeurs utiles à reconnaître sont 09 pour la tabulation, 0A pour le saut de ligne, 0D pour le retour chariot et 20 pour l'espace ordinaire. En UTF-8, C2A0 représente l'espace insécable.

La classe [[:space:]] utilisée par les expressions régulières SQL couvre les caractères d'espacement courants, dont la tabulation, le retour chariot et le saut de ligne. Elle ne couvre pas l'espace insécable UTF-8 C2A0. Ce caractère nécessite donc un traitement spécifique lorsque les données peuvent provenir d'un copier-coller HTML, d'un document bureautique ou d'une source qui insère des espaces insécables.

Le contrôle par HEX() doit être appliqué avant toute correction lorsque le résultat visuel et le résultat d'un appariement divergent. Il permet d'identifier la catégorie exacte du caractère présent et de vérifier ensuite que la normalisation a supprimé les octets attendus.

Normaliser les extrémités d'une référence

La normalisation suivante supprime les séquences d'espacement placées au début ou à la fin de la colonne. La condition limite la mise à jour aux lignes qui nécessitent une correction.

UPDATE produits
   SET ref = REGEXP_REPLACE(ref, '^[[:space:]]+|[[:space:]]+$', '')
 WHERE ref REGEXP '^[[:space:]]+|[[:space:]]+$';

REGEXP_REPLACE est disponible depuis MariaDB 10.0.5 et MySQL 8.0. L'expression cible séparément le début et la fin de la chaîne ; elle ne modifie pas les espaces internes à une référence. Cette règle convient lorsque ces espaces internes appartiennent potentiellement à la valeur métier.

Une alternative portable consiste à utiliser TRIM(BOTH CHAR(9) FROM TRIM(ref)). Elle retire l'espace ordinaire par le premier TRIM(), puis les tabulations aux extrémités. Cette alternative ne couvre pas, à elle seule, tous les caractères traités par [[:space:]].

La normalisation des données existantes doit précéder l'ajout de règles de cohérence. Elle évite de conserver des clés distinctes uniquement par leurs caractères périphériques et réduit les écarts entre les différents systèmes qui alimentent une même table.

Effets dans l'application

Un tableau associatif PHP indexé par une référence non normalisée contient bien la donnée chargée, mais cette donnée devient inatteignable si la lecture est effectuée avec la référence normalisée. Une clé contenant une tabulation initiale et la même clé sans tabulation sont deux index différents. L'échec est un défaut d'appariement, non une absence de chargement.

Les contrôles d'existence applicatifs sont concernés par le même mécanisme. Un test tel que WHERE ref LIKE '$ref' ne trouve pas une valeur préfixée par un caractère invisible lorsque la recherche emploie la valeur propre. La création d'un enregistrement peut alors être autorisée alors qu'une donnée logiquement équivalente existe déjà. Une donnée non normalisée compromet donc les contrôles qui reposent sur elle ; elle ne peut pas être compensée durablement par le seul contrôle applicatif.

PHP 8 ajoute un cas de masquage possible lorsque l'échec d'appariement renvoie une valeur de repli non numérique, par exemple '-'. Dans une condition telle que if ($valeur < 1), PHP 8 compare '-' à '1' en convertissant l'entier en chaîne ; la condition est vraie. L'état d'absence peut alors être interprété comme un résultat métier plausible. Une valeur numérique et un état d'absence doivent être représentés et traités séparément.

Protéger les écritures dans PHP

La validation applicative est le premier niveau de protection. La fonction trim() de PHP retire les caractères " \t\n\r\0\x0B", y compris la tabulation.

$ref = trim($_POST['ref']);

Cette règle est visible dans le code, versionnée et testable. Elle permet de signaler une saisie invalide ou de choisir explicitement sa correction avant l'écriture. Elle donne également une règle lisible aux personnes qui maintiennent l'application.

Sa portée est limitée au point d'entrée où elle est appliquée. Un import CSV, un cron, une API, phpMyAdmin ou un autre programme écrivant directement dans la base peut contourner cette validation. La protection applicative est donc nécessaire pour le dialogue avec l'utilisateur, mais insuffisante lorsqu'une table est alimentée par plusieurs canaux.

Protéger les écritures avec un trigger SQL

Un trigger exécuté avant chaque insertion et mise à jour applique la même normalisation à tous les canaux d'écriture de la table.

CREATE TRIGGER trg_produits_ref_clean_ins BEFORE INSERT ON produits
FOR EACH ROW
  SET NEW.ref = REGEXP_REPLACE(NEW.ref, '^[[:space:]]+|[[:space:]]+$', '');

CREATE TRIGGER trg_produits_ref_clean_upd BEFORE UPDATE ON produits
FOR EACH ROW
  SET NEW.ref = REGEXP_REPLACE(NEW.ref, '^[[:space:]]+|[[:space:]]+$', '');

La présence des triggers se contrôle avec la commande suivante.

SHOW TRIGGERS LIKE 'produits';

Le trigger couvre les imports, scripts, API et écritures manuelles. En contrepartie, sa règle est moins visible pour une personne qui lit uniquement le code applicatif. Les triggers sont aussi rarement versionnés, peuvent être absents après une restauration partielle et ne permettent pas de transmettre un message à l'utilisateur. Leur existence et leur contenu doivent donc être documentés et inclus dans les procédures d'exploitation.

Lorsque le modèle métier l'autorise, une contrainte UNIQUE complète la normalisation. Elle doit être ajoutée après la correction des données existantes : des doublons déjà présents ou des valeurs vides incompatibles avec la règle empêchent sa création. La contrainte ne remplace pas la normalisation, car elle ne transforme pas une valeur saisie avec un caractère invisible.

Sauvegarder les règles de base de données

Les triggers, procédures stockées et événements planifiés doivent être présents dans les sauvegardes et vérifiés après une restauration.

# Sauvegarde explicite : triggers, procédures stockées et événements planifiés
mysqldump --triggers --routines --events ma_base > ma_base.sql

# Contrôle après restauration
mysql -e "SHOW TRIGGERS FROM ma_base;"

Une restauration qui ne rétablit pas ces objets conserve les données mais retire la protection appliquée à l'écriture. Le contrôle après restauration vérifie que les règles prévues sont bien actives dans la base cible.

Validation applicative et normalisation SQL

La validation applicative et le trigger SQL sont complémentaires. La validation dans PHP rend la règle visible, permet le dialogue avec l'utilisateur et traite le point d'entrée couvert. Le trigger protège la table contre l'ensemble des canaux d'écriture, y compris ceux qui ne passent pas par l'application principale.

La fiabilité repose sur ces deux niveaux, sur le contrôle des octets avec HEX() et, lorsque le modèle le permet, sur une contrainte d'unicité appliquée à des données déjà normalisées.