UUID v1 vs v4 vs v7 : choisir une clé primaire de BD

Publié le 2026-04-13 8 min de lecture

Résumé (TL;DR)

Pour le dire crûment : utiliser l’UUID v4 comme clé primaire d’une table à insertions chaudes est une arme à pied. J’ai récemment regardé une charge de travail PostgreSQL 16 où events.id prenait gen_random_uuid() par défaut (un v4), et chaque INSERT atterrissait sur une feuille B-tree aléatoire — refroidissant shared_buffers et faisant grimper la fragmentation d’index dans les chiffres à un seul chiffre élevés rapportés par pgstattuple. Changer le default de colonne vers un générateur v7 — même type uuid de 16 octets, même index — a réduit la latence INSERT moyenne à environ un tiers de ce qu’elle était, et les chiffres de fragmentation se sont stabilisés. Choisir une version d’UUID est une décision de conception de base de données, pas une décision de cryptographie. Un UUID est une valeur 128 bits écrite en chiffres hexadécimaux 8-4-4-4-12, avec quatre bits fixés comme version et deux ou trois bits fixés comme variant. La version 1 estampille l’heure et un identifiant de nœud (historiquement une adresse MAC) dans ces 128 bits — à peu près triable, mais elle fuit l’hôte. La version 4 remplit les bits non réservés avec des données aléatoires : forte non-devinabilité, mais pas d’ordre, donc les insertions atterrissent à des positions d’index arbitraires. La version 7, formalisée dans la RFC 9562 (2024), met un timestamp Unix en millisecondes dans les bits hauts et une queue aléatoire, combinant la sécurité de v4 avec la localité d’index de v1. Pour une API publique où la randomness opaque importe vraiment, v4 reste la valeur par défaut. Presque partout ailleurs, v7 est la meilleure clé primaire. v1 est legacy — accepté par les bases de données, mais ne devrait pas être un nouveau choix.

Contexte et concepts

Un UUID fait 128 bits. Par convention, il est imprimé comme 32 chiffres hexadécimaux groupés par des tirets : xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx. Le chiffre M contient la version (1, 4, 7, etc.) et les bits hauts de N contiennent le variant. Tout le reste est spécifique à la version.

La version 1 a été conçue pour l’unicité à travers les machines et le temps. Son timestamp 60 bits compte les intervalles de 100 nanosecondes depuis le 1582-10-15, un champ de séquence d’horloge gère les retours en arrière d’horloge, et un ID de nœud de 48 bits était à l’origine l’adresse MAC du réseau. La conséquence sur la confidentialité est directe : un UUID v1 frappé sur votre ordinateur encode le MAC de votre ordinateur, et une ligne uuid -d l’inverse. Les bibliothèques modernes randomisent parfois l’ID de nœud pour éviter la fuite, mais beaucoup suivent encore la règle originale.

La version 4 est 122 bits d’aléa avec 6 bits fixés pour la version et le variant. Elle suppose un RNG fort (le crypto.randomUUID() du navigateur ou le gen_random_uuid() de PostgreSQL). Les collisions sont effectivement impossibles à toute échelle réaliste — la borne des anniversaires donne environ une probabilité d’un sur mille milliards après avoir généré environ un milliard de v4. L’inconvénient est que les v4 successifs ne sont pas corrélés, donc les insérer dans un index touche des pages aléatoires, nuisant à la localité du cache et à l’amplification d’écriture (WAL plus écritures de pages complètes dans Postgres).

La version 7 fait partie de la RFC 9562, publiée en 2024, qui a obsolété la RFC 4122 et ajouté les versions 6, 7 et 8. Un UUID v7 stocke un timestamp Unix en millisecondes de 48 bits dans ses bits hauts, suivi de la balise de version, d’un petit champ rand_a, de la balise de variant et d’une queue rand_b de 62 bits. L’effet pratique est que les valeurs v7 générées dans la même milliseconde sont adjacentes dans l’ordre de tri, et les valeurs à travers les millisecondes se trient chronologiquement. La queue aléatoire fournit encore bien plus qu’assez d’entropie pour l’unicité dans une milliseconde. PostgreSQL 18 livre un uuidv7() natif ; sur les versions antérieures, vous pouvez utiliser l’extension pg_uuidv7 ou les générer dans la couche application (Node uuid 9.x, Python uuid6).

Les bits de variant comptent parce qu’ils distinguent la famille RFC 9562 / 4122 des UUID Microsoft et Apollo historiques. Pour ce guide, supposons le variant RFC (le premier chiffre hexadécimal du groupe N est 8, 9, a ou b).

Le format de stockage est une préoccupation séparée. Le type uuid natif de PostgreSQL 16 stocke la valeur comme 16 octets ; MySQL utilise typiquement BINARY(16) ou CHAR(36) — la forme chaîne double le stockage et rend les comparaisons caractère par caractère plutôt qu’octet par octet. Le choix de version et le choix de format de stockage interagissent : trier v7 comme valeur binaire est bon marché et correct, le trier comme chaîne hex est correct mais plus lent, et trier v4 n’a pas de sens quel que soit le stockage.

Comparaison et données

Propriétév1v4v7
Entrée de générationTimestamp + séquence d’horloge + ID de nœud122 bits aléatoiresTimestamp Unix ms 48 bits + queue aléatoire
ConfidentialitéFuit l’ID de nœud (souvent le MAC)Pas d’info d’hôte ou de tempsFuit le temps de création en ms, pas d’hôte
Triable par tempsOui (mais ordre des octets ≠ ordre temporel sans réarrangement)NonOui — l’ordre lexicographique correspond à l’ordre chronologique
Localité d’indexModéréeFaible (insertions aléatoires à travers le B-tree)Bonne (quasi monotone)
Cas d’usage typiqueSystèmes legacy, certains IDs COM/WindowsIDs d’API publique, tokens de session, selsLogs d’événements, insertions à haut volume, tables paginées dans le temps
EntropieFaible (la plupart des bits sont temps/nœud)Haute (environ 122 bits)Queue haute (environ 74 bits), faible collision dans la ms

Un modèle mental approximatif : v4 maximise la non-devinabilité au prix du comportement d’index, v7 garde assez de non-devinabilité pour la plupart des applications tout en restaurant le motif d’insertion-à-la-fin que les bases de données adorent, et v1 est un artefact historique qui mérite d’être reconnu mais pas choisi.

Scénarios concrets

Scénario 1 — Log d’événements en append massif. C’est exactement la charge de l’anecdote d’ouverture. Une table qui ingère des millions de lignes par jour et est généralement interrogée comme « dernières 24 heures, triées par temps » bénéficie directement de v7. Les nouvelles lignes atterrissent à la fin de l’index de clé primaire, donc les pages chaudes restent chaudes et les range scans sur des plages temporelles mappent sur des segments d’index contigus. Migrer de v4 à v7 ici réduit souvent la latence d’écriture et la fragmentation d’index sans changer aucun code de requête — typiquement un changement de default de colonne d’une ligne.

Scénario 2 — IDs publics face à l’utilisateur. Les liens de partage comme /orders/{id} doivent être non devinables pour que les visiteurs ne puissent pas énumérer les commandes d’autres utilisateurs. v4 est la valeur par défaut sûre. Si vous voulez aussi les bénéfices de v7, soyez conscient qu’un v7 révèle son timestamp de création à la milliseconde, ce qui peut convenir pour des commandes mais pourrait fuiter des signaux métier (par exemple, le volume exact de paiements par minute) dans des contextes plus sensibles. Le compromis que j’ai recommandé à des équipes est un motif de double ID : v7 en interne pour la clé primaire, et un v4 séparé ou un court slug aléatoire exposé au monde extérieur. Vous gardez le comportement d’index sans fuiter le timing à l’extérieur.

Scénario 3 — Systèmes multi-régions ou shardés. Le préfixe timestamp de v7 signifie que deux régions générant des UUID à la même milliseconde s’entrelaceront proprement par temps, mais dans une milliseconde il n’y a pas de garantie d’ordre à travers les régions. Si vous avez besoin d’un ordre inter-régions plus strict, ULID (un timestamp 48 bits + 80 bits d’aléa encodés en Crockford Base32) a des propriétés quasi identiques et une forme textuelle plus compacte de 26 caractères. Les IDs de style Snowflake vont plus loin en incluant un ID de machine explicite pour un ordre par nœud (le design original de Twitter et la variante de Discord sont tous deux 64 bits), au prix d’exiger une coordination pour allouer ces IDs.

Idées fausses courantes

« Les UUID sont toujours lents dans les bases de données. » Ils sont plus lents qu’un int de 4 octets en termes bruts, mais le coût réel vient de la fragmentation d’insertion aléatoire dans les index B-tree, que v7 élimine largement. Stocker les UUID comme 16 octets au lieu d’une chaîne de 36 caractères coupe la taille d’index en deux et accélère les comparaisons. Beaucoup de benchmarks « les UUID sont lents » sont en réalité « v4 stocké comme CHAR(36) dans MySQL est lent ».

« v4 est la seule version sécurisée. » La queue aléatoire de v7 est encore un grand pool d’entropie, et pour la plupart des applications — références de session, IDs d’API — elle est suffisamment non devinable pour qu’un attaquant ne les énumère pas. Là où la devinabilité compte est le préfixe timestamp : v7 révèle quand la ligne a été créée. Si c’est acceptable (et ça l’est généralement), v7 est un choix raisonnable même pour les IDs externes.

« Les UUID doivent être stockés comme chaînes. » La forme chaîne est 36 caractères (32 hex plus 4 tirets) vs 16 octets en binaire. Le binaire est plus compact et se trie correctement comme octets, ce qui importe pour v1 avec son ordre d’octets non monotone et pour v7 où l’ordre d’octets devrait s’aligner avec le temps. Le type uuid de PostgreSQL stocke déjà la valeur en 16 octets, donc il n’y a rien d’extra à penser là.

« La fuite de MAC de v1 n’importe pas parce que personne ne regarde. » Le MAC dans un UUID v1 est une inversion connue — uuid -d et tout outil forensique l’extrait. Si vos UUID apparaissent dans des URL, des tickets de support ou des dumps de log partagés avec des tiers, c’est une vraie divulgation d’information.

Liste de vérification

  1. Cet UUID apparaîtra-t-il dans un index de base de données qui prend des insertions fréquentes ? Préférez v7. Rabattez-vous sur v4 seulement si la non-devinabilité du temps de création est essentielle.
  2. L’UUID est-il visible pour les utilisateurs finaux ou les partenaires ? L’une ou l’autre version fonctionne ; confirmez simplement que la fuite de timestamp de v7 est acceptable pour le contexte.
  3. Êtes-vous sur Postgres ? Stockez comme uuid (16 octets). Sur MySQL, utilisez BINARY(16) à moins que la compatibilité chaîne ne l’emporte sur le coût de taille.
  4. Avez-vous besoin de garanties d’ordre à travers plusieurs générateurs ? v7 seul ne suffit pas ; envisagez ULID avec le même préfixe temporel, ou un ID de style Snowflake avec un ID de machine explicite.
  5. v1 est-il encore dans la base de code ? Documentez la fuite de MAC et planifiez une migration quand le schéma le permet.
  6. Générez-vous des UUID côté client ? Utilisez une bibliothèque qui appelle un RNG cryptographiquement fort (crypto.randomUUID() dans les navigateurs modernes pour v4 ; les bibliothèques v7 enveloppent généralement le même RNG).

Outil associé

Le générateur d’UUID Patrache Studio produit des UUID v4 et v7 localement, donc les valeurs générées ne sont jamais journalisées à un service tiers. Les UUID voyagent presque toujours à l’intérieur de payloads JSON — Formatage, validation et schéma JSON en pratique couvre les motifs de schéma qui gardent ces IDs bien typés pendant qu’ils se déplacent entre services. Quand vous avez besoin d’une forme textuelle compacte — par exemple, un ID court de 22 caractères dérivé d’un UUID de 16 octets — les règles dans Base64 et encodage d’URL : but, pièges, usage correct expliquent pourquoi Base64URL plutôt que Base64 standard est le bon choix.

Références