Skip to content
Retour au blog
Tutoriels

Qu'est-ce qu'un ULID ? Guide de l'identifiant triable

Qu'est-ce qu'un ULID ? Comment fonctionne cet identifiant 128 bits triable : sa structure horodatage plus aléa, l'encodage Crockford Base32 et quand le préférer à un UUID.

12 min de lecture

Qu’est-ce qu’un ULID ? L’identifiant unique triable, expliqué

Chaque UUIDv4 aléatoire que vous insérez comme clé primaire atterrit à un endroit imprévisible de l’index de la base de données. Répétez l’opération quelques millions de fois et l’index se fragmente, le cache se met à thrasher et les écritures ralentissent. Un ULID corrige ce défaut sans vous priver de ce qui vous plaisait dans les UUID : vous pouvez toujours en générer un n’importe où, sans coordinateur central, mais il se range dans l’ordre du temps au lieu de se disperser.

Comment une chaîne de 26 caractères parvient-elle à se trier toute seule par ordre chronologique ? C’est tout le tour de force, et il vaut mieux le comprendre avant d’y recourir.

Un ULID (Universally Unique Lexicographically Sortable Identifier) est un identifiant de 128 bits écrit sous la forme de 26 caractères Crockford Base32. Les 10 premiers caractères encodent un horodatage en millisecondes et les 16 derniers encodent des bits aléatoires : ainsi, les ULID créés plus tard se trient toujours après les plus anciens lorsqu’on les compare comme de simples chaînes de caractères. C’est un identifiant unique triable que vous pouvez générer hors ligne.

Ce guide le décortique : l’anatomie décodée caractère par caractère, la preuve qu’il se trie vraiment, le calcul d’arbre B derrière le gain en base de données, et un examen honnête de ce que révèle l’horodatage incorporé. Vous pouvez suivre avec une valeur en direct dans le Générateur ULID — générez-en un, décodez-le, convertissez-le en UUID — pendant votre lecture.

Qu’est-ce qu’un ULID ?

Un ULID (Universally Unique Lexicographically Sortable Identifier) est un identifiant de 128 bits conçu comme une alternative plus triable et plus compacte à un UUID. Il s’écrit avec 26 caractères Crockford Base32 : les 10 premiers contiennent un horodatage de 48 bits en millisecondes depuis l’Unix epoch, et les 16 restants contiennent 80 bits d’aléa. Comme le temps vient en premier, la chaîne se trie chronologiquement.

Cette dernière propriété est la raison d’être du format. UUIDv4 est entièrement aléatoire, ce qui est excellent pour l’unicité mais signifie que deux identifiants créés à une seconde d’intervalle n’ont aucune relation entre eux. Les ULID conservent le modèle « générer n’importe où, sans coordination » et y ajoutent l’ordre temporel : une colonne d’ULID est donc naturellement triée par date de création, sans rien de plus.

Voici le format en un coup d’œil :

PropriétéValeur
Bits128
Encodage26 caractères Crockford Base32
Dispositionhorodatage 48 bits + aléa 80 bits

Le reste de cet article explique comment chaque pièce fonctionne. L’encodage et la triabilité méritent leurs propres sections : nous arriverons donc bientôt au Base32 et à la preuve de l’ordre. D’abord, la disposition.

Anatomie d’un ULID : 48 bits de temps + 80 bits d’aléa

Les 26 caractères d’un ULID se divisent nettement en deux moitiés. Les 10 premiers caractères sont l’horodatage ; les 16 derniers sont la partie aléatoire. Disposez l’exemple canonique et la frontière saute aux yeux :

01ARYZ6S41   TSV4RRFFQ69G5FAV
└────────┘   └──────────────┘
 10 chars        16 chars
48-bit ms      80-bit random
timestamp

Deux composants, deux rôles. L’un enregistre le quand ; l’autre garantit l’unicité. Décodons chacun d’eux.

L’horodatage de 48 bits (10 premiers caractères)

Les 10 premiers caractères encodent un entier de 48 bits : le nombre de millisecondes depuis l’Unix epoch au moment où l’ULID a été créé. Prenez l’exemple canonique directement issu de la spécification :

01ARYZ6S41  ->  1469918176385 ms  ->  2016-07-30T22:36:16.385Z

C’est un décodage réel et réversible : collez 01ARYZ6S41TSV4RRFFQ69G5FAV dans un décodeur et vous récupérez exactement 2016-07-30T22:36:16.385Z. Le composant temporel est une donnée brute, pas un hachage : sa lecture ne coûte donc rien.

Un petit détail qui déroute : le premier caractère d’un ULID est toujours compris entre 0 et 7. Un caractère Crockford porte 5 bits, et 48 bits n’est pas un multiple de 5 : l’horodatage occupe les 48 bits de poids faible parmi les 50 bits que 10 caractères peuvent porter, ce qui laisse les 2 bits de poids fort du premier caractère définitivement à zéro. Deux bits à zéro plafonnent la valeur de ce caractère à 7. Si vous voyez un jour un ULID commençant par 8 ou plus, il est mal formé.

Les 80 bits d’aléa (16 derniers caractères)

Les 16 caractères restants portent 80 bits d’aléa, et c’est de cette moitié que vient l’unicité. Les bits doivent provenir d’une source cryptographiquement sûre — crypto.getRandomValues dans le navigateur, et non Math.random. La différence compte : Math.random est assez prévisible pour qu’un attaquant puisse deviner ou faire entrer en collision des valeurs, alors qu’un CSPRNG ne l’est pas.

Quelle marge représentent 80 bits ? Environ 1,2 × 10²⁴ valeurs possibles, et ce par milliseconde. Même si vous générez des millions d’ULID au sein d’une seule milliseconde, la probabilité que deux d’entre eux tirent les mêmes 80 bits reste infinitésimale. Contrairement à l’horodatage, cette moitié ne porte aucun sens décodable : c’est du bruit dont le seul but est de rendre chaque ULID distinct.

Crockford Base32 : pourquoi les ULID écartent I, L, O et U

Les ULID sont encodés avec le Crockford Base32, un alphabet de 32 symboles : les chiffres 09 et les lettres AZ dont quatre sont retirées.

0123456789ABCDEFGHJKMNPQRSTVWXYZ

Les lettres manquantes sont I, L, O et U. Trois sont écartées parce qu’elles ressemblent à des chiffres — I et L ressemblent à 1, O ressemble à 0 — afin qu’une personne lisant un ULID à l’écran ne confonde pas une lettre avec un chiffre. L’avantage en retour est une saisie tolérante : un décodeur conforme reconvertit I et L en 1 et O en 0, et traite l’ensemble de la chaîne sans tenir compte de la casse. U est exclue séparément, pour éviter de former par accident des mots offensants.

Le calcul de bits est l’autre raison. Chaque caractère Base32 encode 5 bits, là où un caractère hexadécimal n’en encode que 4. Empaquetez 128 bits à raison de 5 bits par caractère et il vous en faut 26 ; empaquetez les mêmes 128 bits à 4 bits chacun — comme le fait un UUID — et il vous en faut 32, plus quatre traits d’union, soit 36 caractères. Un ULID est donc nettement plus court qu’un UUID et, sans traits d’union, se glisse directement dans une URL, un nom de fichier ou un en-tête sans échappement.

Le Crockford Base32 est un alphabet de 32 symboles (09 et AZ moins I, L, O, U) qui encode 5 bits par caractère. Les ULID l’utilisent pour empaqueter 128 bits en 26 caractères insensibles à la casse et compatibles avec les URL et — c’est crucial — l’alphabet est en ordre croissant, ce qui permet à la chaîne encodée de se trier de la même façon que les bits bruts.

Pourquoi les ULID se trient par ordre chronologique

Beaucoup d’articles vous disent que les ULID se trient par ordre chronologique. Peu montrent pourquoi : voici donc l’argument réel. Il repose sur deux faits que vous avez déjà : l’horodatage est la partie la plus significative de la valeur, et l’alphabet de Crockford est disposé en ordre croissant.

Mettez les deux ensemble et vous obtenez une chaîne d’équivalences :

string compare  ==  128-bit integer compare  ==  creation-time compare

Lisez-la de gauche à droite. Comparer deux ULID caractère par caractère (comme le fait un tri de chaînes) donne la même réponse que comparer leurs entiers 128 bits sous-jacents, parce que l’alphabet préserve l’ordre — un caractère « plus haut » signifie toujours une valeur plus grande. Comparer les entiers 128 bits donne la même réponse que comparer les dates de création, parce que l’horodatage occupe les bits de poids fort et domine donc la comparaison ; la queue aléatoire ne départage que les égalités au sein d’une même milliseconde. Ordre des chaînes, ordre des bits et ordre temporel sont le même ordre.

Une démonstration rapide. Deux ULID générés à une milliseconde d’intervalle :

01ARYZ6S41...   (created at T)
01ARYZ6S42...   (created at T + 1 ms)

Le dixième caractère passe de 1 à 2, et un simple tri textuel place le second après le premier — sans colonne d’horodatage, sans comparateur spécial. Le bénéfice concret, que la section suivante développe, tient en une ligne : ORDER BY id renvoie les lignes dans l’ordre chronologique sans index supplémentaire.

Les ULID comme clés primaires de base de données : localité dans l’arbre B

C’est là que les ULID justifient leur existence. La plupart des bases de données relationnelles stockent l’index de clé primaire sous forme d’arbre B, et l’endroit où une nouvelle clé atterrit dans cet arbre détermine le coût de l’insertion.

Un UUIDv4 aléatoire atterrit à un endroit imprévisible à chaque insertion :

UUIDv4 : chaque nouvelle clé vise une page feuille aléatoire. La page est souvent pleine, alors le moteur la scinde, copie la moitié des lignes ailleurs et salit des pages dans tout l’arbre. Sur des millions de lignes, cela fragmente l’index, expulse des pages utiles du cache tampon et fait chuter le débit d’insertion. (Pour les chiffres précis des scissions de pages d’index — généralement un écart de 2 à 10× sur les tables à écritures intensives — consultez le guide comparatif.)

Un ULID préfixé par le temps atterrit à la fin à chaque fois :

ULID : parce que les bits de poids fort sont un horodatage, chaque nouvelle clé est supérieure à la précédente : elle s’ajoute donc à l’extrémité droite de l’index, ou tout près. Les insertions restent séquentielles, les scissions de pages disparaissent presque, l’index reste compact, et un balayage de plage sur une fenêtre temporelle lit une suite contiguë de pages.

Vous obtenez la génération sans coordination d’un UUID avec la localité d’insertion d’un entier auto-incrémenté — sans exposer un compteur séquentiel devinable, puisque la queue aléatoire masque toujours la valeur exacte suivante.

Astuce de stockage : stockez les 128 bits sous forme de 16 octets binaires — une colonne uuid en PostgreSQL, BINARY(16) en MySQL — et non comme un champ texte de 26 caractères, qui gaspille de l’espace et fait gonfler l’index. N’encodez en chaîne Base32 qu’aux bordures où un humain ou une URL la voit. L’onglet Convertir du générateur permet de convertir un ULID en UUID précisément pour cela, puisque les deux formes sont les mêmes 128 bits.

ULID monotones : ordre strict au sein d’une milliseconde

La preuve de triabilité a une faille honnête : au sein d’une seule milliseconde, les ULID ordinaires ne sont pas strictement ordonnés. Ils partagent le même préfixe temporel de 10 caractères, mais leurs queues aléatoires de 80 bits sont tirées indépendamment : lequel de deux ULID de la même milliseconde se trie en premier relève donc essentiellement du pile ou face. Pour la plupart des usages, c’est sans importance. Quand vous avez besoin d’un ordre strict même à des cadences inférieures à la milliseconde, ça ne l’est pas.

La génération monotone comble la faille. La règle est simple : le premier ULID d’une milliseconde donnée reçoit un aléa frais comme d’habitude, et chaque ULID suivant dans cette même milliseconde est produit en prenant la valeur aléatoire de 80 bits précédente et en l’incrémentant de un (traitée comme un entier big-endian, avec report dans les bits supérieurs si nécessaire). Chaque valeur est donc strictement supérieure à la précédente.

Vous pouvez le constater dans un lot généré au sein d’une seule milliseconde — seul le dernier caractère bouge :

01KVT0F720ZK9N4T2QX7VR8WMC
01KVT0F720ZK9N4T2QX7VR8WMD
01KVT0F720ZK9N4T2QX7VR8WME

…WMC < …WMD < …WME, garanti. Cela compte chaque fois que des lignes peuvent être créées plus vite que ne bat l’horloge de la milliseconde : insertions à haut débit, journaux d’événements, identifiants de messages dans une boucle serrée. Lorsque l’horloge avance à la milliseconde suivante, la génération revient à un aléa frais et le cycle recommence.

ULID vs UUID : lequel utiliser et quand

La question avec laquelle la plupart des gens arrivent, c’est ULID vs UUID. Voici la comparaison ciblée : l’ULID face aux deux versions d’UUID que vous mettriez réalistement dans la balance. (Pour la matrice de décision complète à cinq options incluant Snowflake et NanoID, voyez la comparaison complète d’ULID, UUID et Snowflake.)

PropriétéULIDUUIDv4UUIDv7
Longueur26 caractères36 caractères36 caractères
EncodageCrockford Base32Hexadécimal avec traits d’unionHexadécimal avec traits d’union
Triable par temps ?OuiNonOui
Intègre un horodatage ?Oui (48 bits ms)NonOui (48 bits ms)
Normalisé ?Spécification communautaireRFC 9562RFC 9562
Idéal pourIdentifiants triables courtsIdentifiants aléatoires opaquesIdentifiants triables au format UUID

En clair : optez pour un ULID lorsque vous voulez la chaîne triable la plus courte et compatible avec les URL. Optez pour un UUIDv4 lorsque vous voulez un identifiant opaque, entièrement aléatoire et sans temps incorporé — par exemple un jeton public dont vous préférez ne pas révéler la date de création. Optez pour UUIDv7 lorsque vous avez besoin de l’ordre temporel mais que vous devez rester dans le format UUID standard, avec les bits de version et de variante à leurs positions fixes et une colonne uuid native où le déposer.

Les trois font 128 bits, donc la conversion ULID ↔ UUID est sans perte dans les deux sens. La relation entre ULID et ulid vs uuid v7 est plus étroite qu’il n’y paraît : UUIDv7 est essentiellement la version normalisée par l’IETF de la même idée d’identifiant préfixé par le temps qu’ULID a inaugurée. Si vous débutez avec les UUID, commencez d’abord par les fondamentaux, puis revenez à cette comparaison.

Le compromis sur la confidentialité : les ULID révèlent leur date de création

L’horodatage incorporé est à la fois une fonctionnalité et une fuite, selon qui lit l’identifiant. Quiconque détient un ULID peut décoder l’horodatage en une seule étape et apprendre la milliseconde exacte à laquelle l’enregistrement a été créé — sans aucun accès à votre base de données.

À l’intérieur de vos propres systèmes, c’est un pur avantage : vous lisez la date de création directement dans l’identifiant, ce qui aide à l’audit comme au débogage. Sur un identifiant exposé publiquement, c’est une véritable divulgation. La date de création peut être sensible en soi sur le plan commercial, et quelques ULID échantillonnés dans le temps révèlent votre cadence de création — combien de commandes, de comptes ou de messages vous générez par seconde — le genre de chose que concurrents et scrapers aiment estimer.

Pour être juste, c’est une fuite plus restreinte que celle d’UUIDv1, qui incorporait historiquement l’adresse MAC de la machine génératrice ; un ULID n’expose que le temps, jamais l’identité matérielle. Pesez-le tout de même. La mesure d’atténuation simple : gardez les ULID en interne et distribuez un UUIDv4 entièrement aléatoire pour les identifiants exposés publiquement où l’ordre n’a pas d’importance.

Pièges courants avec les ULID

La plupart des ennuis avec les ULID viennent d’une poignée de décisions d’ingénierie évitables, pas de bugs du format. Les récurrents :

  • Supposer que les ULID ordinaires d’une même milliseconde sont ordonnés. Ils partagent un préfixe temporel mais ont des queues aléatoires indépendantes : leur ordre est donc indéfini. Correctif : utilisez le mode monotone quand vous avez besoin d’un ordre strict à des cadences inférieures à la milliseconde.
  • Stocker un ULID en texte de 26 caractères. Cela gaspille de l’espace et fait gonfler l’index. Correctif : stockez les 128 bits sous forme de 16 octets (uuid / BINARY(16)) et n’encodez en Base32 qu’aux bordures.
  • Attendre qu’une conversion ULID→UUID se signale comme v4 ou v7. La conversion réencode les mêmes bits ; elle ne définit pas les champs de version et de variante de l’UUID, donc une bibliothèque qui les inspecte n’y verra pas de version étiquetée. Correctif : traitez le résultat comme une valeur 128 bits opaque, ou générez un vrai UUIDv7 quand vous avez besoin de l’étiquette.
  • Remplir l’aléa avec Math.random. Il est prévisible et peut entrer en collision. Correctif : utilisez toujours un CSPRNG comme crypto.getRandomValues.
  • Exposer des ULID publiquement sans peser la fuite d’horodatage. Voyez la section sur la confidentialité ci-dessus. Correctif : ULID en interne, UUIDv4 aléatoire pour les identifiants publics.
  • Taper à la main I, L, O ou U dans un ULID. Ces lettres ne sont pas dans l’alphabet, et les ressaisir invite aux erreurs. Correctif : copiez les ULID, ne les ressaisissez pas.

FAQ

ULID est-il une norme officielle comme UUID ?

Non. ULID est une spécification communautaire publiée sur GitHub, et non une RFC de l’IETF. Il est largement implémenté et stable, mais aucun organisme de normalisation ne le soutient. Si vous avez besoin d’un identifiant normalisé et ordonné dans le temps, UUIDv7 (RFC 9562) applique la même idée à l’intérieur du format UUID officiel.

Combien de caractères compte un ULID, et pourquoi est-il plus court qu’un UUID ?

26 caractères, contre 36 pour un UUID. ULID utilise le Crockford Base32, qui empaquette 5 bits par caractère ; l’hexadécimal d’un UUID n’en empaquette que 4 et ajoute quatre traits d’union. Les mêmes 128 bits nécessitent donc moins de caractères en Base32 — et aucun d’eux ne requiert d’échappement d’URL.

Deux ULID peuvent-ils entrer en collision ?

En pratique, jamais. Au sein d’une milliseconde, un ULID possède 80 bits aléatoires — environ 1,2 × 10²⁴ possibilités — donc même en en générant des millions par milliseconde, la probabilité de collision reste infinitésimale. La seule exigence est qu’un RNG cryptographiquement sûr remplisse l’aléa ; Math.random annule la garantie.

Puis-je stocker des ULID dans PostgreSQL ou MySQL ?

Oui. Un ULID fait 128 bits : convertissez-le en forme UUID et stockez-le dans une colonne uuid (PostgreSQL) ou BINARY(16) (MySQL), puis n’affichez la chaîne Base32 qu’aux bordures. Il n’existe pas de type de colonne ULID natif, mais la représentation UUID coûte les mêmes 16 octets et garde l’index compact.

Les ULID sont-ils sensibles à la casse ?

La forme canonique est en majuscules, mais le Crockford Base32 est insensible à la casse en entrée : un décodeur lit les lettres minuscules de la même façon, et convertit I/L en 1 et O en 0. Pour éviter les surprises dans les tests d’égalité et les index, normalisez vers une seule casse avant de stocker ou de comparer.

L’horodatage de 48 bits finira-t-il par être épuisé ?

Pas avant très longtemps. 48 bits de millisecondes atteignent l’année 10889 avant que le compteur ne déborde : le composant horodatage est donc pratiquement pérenne pour toute application réelle. Vous remplacerez le système, le langage et la base de données bien avant que le format ne manque de place.

Puis-je générer des ULID dans le navigateur ou sur mobile sans serveur ?

Oui — c’est un avantage central. Les ULID n’ont besoin d’aucun coordinateur central : tout nœud, edge worker, navigateur ou appareil peut en générer un à partir de son horloge plus un RNG sûr. Les valeurs créées sur des machines différentes se trient quand même ensemble par ordre chronologique ensuite, parce que l’horodatage vit dans l’identifiant lui-même.

Conclusion

Les ULID résolvent un problème précis et réel — des clés aléatoires qui fragmentent votre index — sans supprimer la génération décentralisée. Les mécanismes valent la peine d’être gardés en tête :

  • Un ULID est un horodatage de 48 bits en millisecondes + 80 bits d’aléa, encodé en 26 caractères Crockford Base32.
  • Il se trie par ordre chronologique parce que l’horodatage est le composant le plus significatif et que l’alphabet préserve l’ordre — l’ordre des chaînes équivaut à l’ordre temporel.
  • Cet ordre confère à un arbre B la localité d’insertion qui manque à un UUIDv4 aléatoire, gardant les écritures rapides et l’index compact.
  • Utilisez le mode monotone quand vous avez besoin d’un ordre strict pour des identifiants générés dans la même milliseconde.
  • Pesez la fuite d’horodatage avant d’exposer des ULID sur des identifiants publics.
  • Préférez UUIDv7 lorsque vous devez rester dans le format UUID standard.

Quand vous êtes prêt à le mettre en œuvre, ouvrez le Générateur ULID pour générer, décoder et convertir des ULID entièrement dans votre navigateur — sans serveur, sans téléversement, rien de stocké.

Tags: ulid uuid unique-identifier database primary-key

Articles connexes

Voir tous les articles