Mettez un UUID sur la facture avant d'en avoir besoin
L'affirmation Donnez à chaque enregistrement d'affaires un identifiant stable et sûr pour l'exposition, dès sa création, distinct de sa clé primaire de base de données. L'entier au...
L'affirmation
Donnez à chaque enregistrement d'affaires un identifiant stable et sûr pour l'exposition, dès sa création, distinct de sa clé primaire de base de données. L'entier auto-incrémenté convient à l'intérieur de votre base et devient dangereux partout ailleurs : il révèle votre volume à vos concurrents, il entre en collision quand vous fusionnez des jeux de données, et il vous force à exposer un nombre qui traduit votre comptage interne. Une colonne UUID coûte une ligne de migration et supprime toute une catégorie de douleurs futures.
Ce que l'entier séquentiel révèle au monde
Une confirmation de commande affichant /orders/1042 indique à n'importe quel client exactement combien de commandes vous avez traitées. Passez deux commandes à une semaine d'intervalle, voyez le nombre bondir de 1042 à 1119, et vous venez de mesurer le volume hebdomadaire d'un concurrent à même son propre courriel de confirmation. Ce n'est pas hypothétique — c'est une étape courante de veille concurrentielle, et la technique a un nom : le problème des chars allemands, résolu pendant la Seconde Guerre mondiale en lisant des numéros de série.
L'entier cause aussi des ennuis d'exploitation. Quand vous acquérez une entreprise et fusionnez sa table de commandes à la vôtre, chaque identifiant entre en collision. Quand une exportation d'essai fuit vers la production, ou qu'une ligne de test est créée dans le mauvais environnement, les séquences dérivent et vous ne pouvez plus vous fier à ce que l'identifiant 5000 soit le même enregistrement partout.
Le motif : deux identifiants, deux rôles
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
public_id uuid NOT NULL DEFAULT gen_random_uuid(),
...
);
CREATE UNIQUE INDEX orders_public_id ON orders (public_id);
Le bigint reste interne : c'est votre cible de clé étrangère, petite et rapide à joindre. Le public_id est ce qui paraît dans les adresses, les courriels, les réponses d'API et les billets de soutien. Rien hors de votre base ne voit jamais l'entier ; rien hors de votre base ne peut donc compter avec.
Choisissez la version d'UUID sciemment
Les UUID aléatoires (version 4) ne révèlent rien, c'est le but, mais des valeurs purement aléatoires se dispersent dans un index B et nuisent à la performance d'insertion sur les grandes tables, chaque nouvelle ligne aboutissant dans une page-feuille au hasard. Si vous générez des millions de lignes, l'UUID version 7 — préfixé d'un horodatage — conserve la confidentialité d'un suffixe aléatoire tout en restant à peu près ordonné dans le temps, gardant les insertions locales dans l'index :
-- Postgres 18 fournit gen_uuid_v7() ; sur les versions anterieures,
-- generez le v7 dans l'application et transmettez-le.
Pour la plupart des entreprises, le coût d'insertion du v4 est invisible et le v4 est le bon choix par défaut. Ne passez au v7 qu'après avoir mesuré l'amplification d'écriture d'index sur une table réellement grande. Ne revenez surtout pas à un entier séquentiel déguisé en identifiant public : cela réintroduit exactement la fuite que vous aviez retirée.
Quand vous voulez plutôt du court et du convivial
Un UUID fait 36 caractères et aucun humain n'en lit un au téléphone. Pour les enregistrements qu'un client cite au soutien — numéros de commande, de billet — un code opaque plus court se lit mieux. Générez-le à partir d'un alphabet défini qui exclut les caractères ambigus :
alphabet : 23456789ABCDEFGHJKLMNPQRSTUVWXYZ (pas de 0/O, 1/I/L)
longueur 8 = ~1 billion de valeurs
Exclure 0/O et 1/I/L n'est pas cosmétique : cela élimine les erreurs de transcription qui engendrent des billets. Stockez ce code aux côtés de l'UUID, non à sa place — l'UUID garantit l'unicité à l'échelle, le code court est la commodité pour l'humain.
La règle qui rend l'exposition sûre
Qu'un identifiant public soit indevinable n'est pas une autorisation. Quiconque a le lien peut l'essayer et, si votre point d'accès remet la commande à qui présente l'identifiant, vous avez construit une faille de contrôle d'accès avec une clé plus longue. L'identifiant public supprime l'énumération — personne ne peut boucler de 1 à 10000 — mais chaque point d'accès doit toujours vérifier que l'utilisateur demandeur possède l'enregistrement. Traitez l'identifiant comme un nom d'utilisateur, jamais comme un mot de passe.
L'ajouter à une table existante
Vous pouvez ajouter cela à une table comptant déjà des millions de lignes sans interruption, avec le motif étendre-migrer-contracter : ajoutez la colonne nullable, remplissez par lots, créez l'index unique de façon concurrente, puis commencez à inscrire la valeur sur les nouvelles lignes. Les anciennes adresses fondées sur l'entier continuent de fonctionner pendant la transition, puisque vous n'avez rien retiré — vous avez seulement ajouté une seconde façon, plus sûre, de désigner le même enregistrement.
ALTER TABLE orders ADD COLUMN public_id uuid;
UPDATE orders SET public_id = gen_random_uuid()
WHERE public_id IS NULL AND id IN (SELECT id FROM orders WHERE public_id IS NULL LIMIT 5000);
-- repeter, puis :
CREATE UNIQUE INDEX CONCURRENTLY orders_public_id ON orders (public_id);
La décision
Ajouter un identifiant public à une table neuve coûte une colonne et un index. L'ajouter à un système vieux de cinq ans qui a laissé fuir des identifiants séquentiels sur dix mille factures, une API publique et une intégration partenaire coûte une migration, une couche de compatibilité et une conversation avec chaque intégrateur. La ligne va dans la première migration, ou elle va dans la migration coûteuse plus tard. Il n'existe aucune version où attendre revient moins cher.