UUID vs ULID vs IDs séquentiels : apprenez comment chaque type affecte l'indexation, le tri, le sharding et les workflows d'export/import dans des projets réels.

Choisir un ID semble ennuyeux la première semaine. Puis vous livrez, les données grossissent, et cette décision « simple » apparaît partout : index, URLs, logs, exports et intégrations.
La vraie question n'est pas « lequel est le meilleur ? », mais « quelle douleur voulez-vous éviter plus tard ? ». Les IDs sont difficiles à changer parce qu'ils sont copiés dans d'autres tables, mis en cache par les clients et dépendus par d'autres systèmes.
Quand l'ID ne correspond pas à l'évolution du produit, vous le remarquez généralement à plusieurs endroits :
Il y a toujours un compromis entre la commodité maintenant et la flexibilité plus tard. Les entiers séquentiels sont faciles à lire et souvent rapides, mais ils peuvent révéler le nombre d'enregistrements et rendre les fusions de jeux de données plus difficiles. Les UUIDs aléatoires sont excellents pour l'unicité entre systèmes, mais ils sont plus pénibles pour les index et pour les humains qui lisent les logs. Les ULIDs visent l'unicité globale avec un tri temporel approximatif, mais ils ont aussi des compromis de stockage et d'outillage.
Une façon utile de réfléchir : pour qui est principalement l'ID ?
Si l'ID est surtout pour des humains (support, debug, ops), des valeurs plus courtes et lisibles l'emportent souvent. S'il est pour des machines (écritures distribuées, clients hors ligne, systèmes multi-région), l'unicité globale et l'évitement des collisions comptent davantage.
Quand on débat « UUID vs ULID vs IDs séquentiels », on choisit en réalité comment chaque ligne obtient une étiquette unique. Cette étiquette affecte la facilité d'insertion, de tri, de fusion et de déplacement des données plus tard.
Un ID séquentiel est un compteur. La base distribue 1, puis 2, puis 3, etc. (souvent stocké en integer ou bigint). C'est facile à lire, peu coûteux en stockage et généralement rapide parce que les nouvelles lignes s'ajoutent à la fin de l'index.
Un UUID est un identifiant 128 bits qui ressemble à du hasard, comme 3f8a.... Dans la plupart des configurations, il peut être généré sans demander à la base de données le numéro suivant, donc différents systèmes peuvent créer des IDs indépendamment. Le compromis est que des inserts d'apparence aléatoire peuvent solliciter davantage les index et prendre plus d'espace qu'un simple bigint.
Un ULID est aussi 128 bits, mais conçu pour être approximativement ordonné dans le temps. Les ULIDs plus récents sont généralement triés après les plus anciens, tout en restant globalement uniques. On obtient souvent une partie du bénéfice « généré n'importe où » des UUIDs avec un comportement de tri plus agréable.
Un résumé simple :
Les IDs séquentiels sont courants pour les applications mono-base et outils internes. Les UUIDs apparaissent quand les données sont créées à travers plusieurs services, appareils ou régions. Les ULIDs sont populaires quand des équipes veulent une génération d'ID distribuée tout en se souciant du tri, de la pagination ou des requêtes « les plus récents d'abord ».
Une clé primaire est généralement soutenue par un index (souvent un B-tree). Pensez à cet index comme à un annuaire trié : chaque nouvelle ligne doit voir son entrée placée au bon endroit pour que les recherches restent rapides.
Avec des IDs aléatoires (UUIDv4 classique), les nouvelles entrées atterrissent un peu partout dans l'index. Cela signifie que la base touche de nombreuses pages d'index, scinde plus souvent les pages et effectue des écritures supplémentaires. Avec le temps, on obtient plus de churn d'index : plus de travail par insert, plus de cache misses et des index plus volumineux que prévu.
Avec des IDs majoritairement croissants (séquentiels/bigint, ou IDs ordonnés par le temps comme beaucoup d'ULIDs), la base peut généralement append les nouvelles entrées près de la fin de l'index. C'est plus friendly pour le cache parce que les pages récentes restent « chaudes », et les inserts sont souvent plus fluides à des débits d'écriture élevés.
La taille de la clé compte car les entrées d'index ne sont pas gratuites :
Des clés plus grandes signifient moins d'entrées par page d'index. Cela mène souvent à des index plus profonds, plus de pages lues par requête et plus de RAM nécessaire pour rester rapide.
Si vous avez une table d'"events" avec des inserts constants, une clé primaire UUID aléatoire peut commencer à paraître plus lente plus tôt qu'une clé bigint, même si les recherches d'une seule ligne semblent encore correctes. Si vous prévoyez de fortes écritures, le coût d'indexation est généralement la première vraie différence que vous remarquerez.
Si vous avez construit un « Charger plus » ou un scroll infini, vous avez déjà ressenti la douleur des IDs qui ne se trient pas bien. Un ID « se trie bien » quand le tri par celui-ci donne un ordre stable et signifiant (souvent l'heure de création) pour que la pagination soit prévisible.
Avec des IDs aléatoires (comme UUIDv4), les nouvelles lignes sont dispersées. Le tri par id ne correspond pas au temps, et la pagination par curseur du type "donnez-moi les éléments après cet id" devient peu fiable. On finit généralement par utiliser created_at, ce qui est correct, mais il faut le faire soigneusement.
Les ULIDs sont conçus pour être approximativement ordonnés dans le temps. Si vous triez par ULID (comme chaîne ou en binaire), les éléments plus récents ont tendance à venir après les plus anciens. Cela rend la pagination par curseur plus simple puisque le curseur peut être le dernier ULID vu.
L'ULID aide à obtenir un ordre temporel approximatif pour les feeds, des curseurs plus simples et moins d'insertion aléatoire que UUIDv4.
Mais l'ULID ne garantit pas un ordre temporel parfait quand beaucoup d'IDs sont générés dans la même milliseconde sur plusieurs machines. Si vous avez besoin d'un ordre exact, vous voulez toujours un vrai timestamp.
created_at reste préférableTrier par created_at est souvent plus sûr quand vous backfillez des données, importez des enregistrements historiques ou avez besoin d'un tie-breaking clair.
Un pattern pratique est d'ordonner par (created_at, id), où id ne sert que de tie-breaker.
Le sharding signifie répartir une base en plusieurs bases plus petites pour que chaque shard contienne une partie des données. Les équipes le font généralement plus tard, quand une seule base est difficile à faire évoluer ou devient un risque en tant que point de défaillance unique.
Votre choix d'ID peut rendre le sharding gérable ou pénible.
Avec des IDs séquentiels (auto-increment serial ou bigint), chaque shard génèrera joyeusement 1, 2, 3.... Le même ID peut exister sur plusieurs shards. La première fois que vous devez fusionner des données, déplacer des lignes ou construire des fonctionnalités inter-shards, vous rencontrez des collisions.
Vous pouvez éviter les collisions avec de la coordination (un service central d'ID, ou des plages par shard), mais cela ajoute des composants et peut devenir un goulot. Les UUIDs et ULIDs réduisent la coordination parce que chaque shard peut générer des IDs indépendamment avec un risque de duplication extrêmement faible. Si vous pensez devoir un jour répartir les données entre bases, c'est un des arguments les plus forts contre les séquences pures.
Un compromis courant est d'ajouter un préfixe de shard puis d'utiliser une séquence locale sur chaque shard. Vous pouvez le stocker en deux colonnes, ou l'encoder dans une seule valeur.
Ça marche, mais ça crée un format d'ID personnalisé. Chaque intégration doit le comprendre, le tri cesse d'avoir un ordre temporel global sans logique supplémentaire, et déplacer des données entre shards peut exiger de réécrire les IDs (ce qui casse les références si ces IDs sont partagés).
Posez-vous une question tôt : aurez-vous besoin un jour de combiner des données de plusieurs bases et de garder les références stables ? Si oui, prévoyez des IDs globalement uniques dès le départ, ou prévoyez un budget pour une migration plus tard.
L'export/import est l'endroit où le choix d'ID cesse d'être théorique. Dès que vous clonez la prod en staging, restaurez une sauvegarde ou fusionnez des données de deux systèmes, vous découvrez si vos IDs sont stables et portables.
Avec les IDs séquentiels, vous ne pouvez généralement pas rejouer des inserts dans une autre base et vous attendre à ce que les références restent intactes à moins de préserver les numéros d'origine. Si vous importez seulement un sous-ensemble de lignes (ex. 200 clients et leurs commandes), vous devez charger les tables dans le bon ordre et garder exactement les mêmes clés primaires. Si quelque chose est renuméroté, les clés étrangères cassent.
Les UUIDs et ULIDs sont générés en dehors de la séquence de la base, donc ils sont plus faciles à déplacer entre environnements. Vous pouvez copier des lignes, garder les IDs et les relations restent cohérentes. Cela aide quand vous restaurez des sauvegardes, faites des exports partiels ou fusionnez des jeux de données.
Exemple : exportez 50 comptes de la production pour déboguer un problème en staging. Avec des clés primaires UUID/ULID, vous pouvez importer ces comptes plus les lignes liées (projets, factures, logs) et tout pointe vers le bon parent. Avec des IDs séquentiels, vous vous retrouvez souvent à construire une table de traduction (old_id -> new_id) et réécrire les clés étrangères pendant l'import.
Pour les imports massifs, les bases comptent plus que le type d'ID :
Vous pouvez prendre une décision solide rapidement si vous vous concentrez sur ce qui vous fera mal plus tard.
Écrivez vos principaux risques futurs. Des événements concrets aident : séparation en plusieurs bases, fusion de données client depuis un autre système, écritures hors ligne, copies fréquentes d'environnements.
Décidez si l'ordre par ID doit correspondre au temps. Si vous voulez le « plus récent d'abord » sans colonnes supplémentaires, l'ULID (ou un équivalent triable par le temps) est adapté. Si trier par created_at vous suffit, UUIDs et séquentiels conviennent.
Estimez le volume d'écriture et la sensibilité des index. Si vous attendez de fortes insertions et que votre index de clé primaire est celui qui est le plus sollicité, un BIGINT séquentiel est généralement le plus doux pour les B-tree. Les UUIDs aléatoires causent plus de churn.
Choisissez un défaut, puis documentez les exceptions. Gardez-le simple : un défaut pour la plupart des tables, et une règle claire pour les dérogations (souvent : IDs publics vs internes).
Laissez de la marge pour changer. Évitez d'encoder du sens dans les IDs, décidez où les IDs sont générés (DB vs appli) et gardez les contraintes explicites.
Le plus grand piège est de choisir un ID parce qu'il est populaire, puis découvrir qu'il entre en conflit avec la façon dont vous interrogez, montez en charge ou partagez les données. La plupart des problèmes apparaissent des mois plus tard.
Échecs fréquents :
123, 124, 125, les gens peuvent deviner des enregistrements voisins et sonder votre système.Signes d'alerte à traiter tôt :
Choisissez un type de clé primaire et tenez-vous-y pour la plupart des tables. Mélanger les types (bigint ici, UUID là) complique les jointures, APIs et migrations.
Estimez la taille d'index à l'échelle prévue. Des clés larges signifient des index primaires plus volumineux et plus d'IO/mémoire.
Décidez comment vous paginerez. Si vous paginez par ID, assurez-vous que l'ID a un ordre prévisible (ou acceptez qu'il n'en ait pas). Si vous paginez par timestamp, indexez created_at et utilisez-le de façon cohérente.
Testez votre plan d'import sur des données proches de la prod. Vérifiez que vous pouvez recréer des enregistrements sans casser les clés étrangères et que les ré-imports ne génèrent pas silencieusement de nouveaux IDs.
Rédigez votre stratégie de collision. Qui génère l'ID (DB ou appli), et que se passe-t-il si deux systèmes créent des enregistrements hors ligne puis se synchronisent ?
Assurez-vous que les URLs publiques et les logs ne fassent pas fuiter des schémas que vous voulez garder privés (comptes d'enregistrements, rythme de création, indices de shard). Si vous utilisez des IDs séquentiels, supposez que les gens peuvent deviner des IDs voisins.
Un fondateur solo lance un CRM simple : contacts, opportunités, notes. Une base Postgres, une appli web, l'objectif est d'expédier.
Au départ, une clé primaire serial bigint semble parfaite. Les inserts sont rapides, les index propres et c'est facile à lire dans les logs.
Un an plus tard, un client demande des exports trimestriels pour un audit, et le fondateur commence à importer des leads d'un outil marketing. Les IDs qui étaient seulement internes apparaissent désormais dans des CSV, des emails et des tickets support. Si deux systèmes utilisent 1, 2, 3..., les fusions deviennent pénibles. Vous finissez par ajouter des colonnes source, des tables de mapping ou réécrire les IDs lors de l'import.
Au bout de deux ans, il y a une appli mobile. Elle doit créer des enregistrements hors ligne puis synchroniser plus tard. Vous avez alors besoin d'IDs pouvant être générés côté client sans parler à la base, et d'un faible risque de collision quand les données arrivent dans différents environnements.
Un compromis qui tient souvent la route :
Si vous hésitez entre UUID, ULID et IDs séquentiels, décidez selon la façon dont vos données vont bouger et croître.
Choix en une phrase pour les cas courants :
bigint séquentielle.Le mix est souvent la meilleure réponse. Utilisez bigint séquentiel pour les tables internes qui ne quittent jamais votre base (tables de jointure, jobs), et utilisez UUID/ULID pour les entités publiques comme users, orgs, invoices et tout ce que vous pourriez exporter, synchroniser ou référencer depuis un autre service.
Si vous construisez sur Koder.ai (koder.ai), il vaut la peine de décider de votre pattern d'ID avant de générer beaucoup de tables et d'APIs. Le mode planification de la plateforme et les snapshots/rollback facilitent l'application et la validation des changements de schéma tôt, tant que le système est encore assez petit pour changer en toute sécurité.
Commencez par identifier la douleur future que vous voulez éviter : inserts lents dus à des écritures aléatoires dans l'index, pagination compliquée, migrations risquées, ou collisions d'ID lors d'imports et de fusions. Si vous prévoyez que les données bougeront entre plusieurs systèmes ou seront créées en plusieurs endroits, choisissez par défaut un ID globalement unique (UUID/ULID) et traitez séparément les préoccupations d'ordre temporel.
Le bigint séquentiel est un excellent défaut quand vous avez une seule base de données, des écritures nombreuses, et que les IDs restent internes. Il est compact, efficace pour les index B-tree et facile à lire dans les logs. L'inconvénient principal est la difficulté pour fusionner des données plus tard sans collisions, et le fait qu'il peut dévoiler le nombre d'enregistrements s'il est exposé publiquement.
Choisissez les UUIDs quand des enregistrements peuvent être créés dans plusieurs services, régions, dispositifs ou clients hors ligne et que vous voulez un risque de collision extrêmement faible sans coordination. Les UUIDs conviennent aussi bien comme IDs publics car ils sont difficiles à deviner. Le compromis habituel est des index plus volumineux et des insertions plus aléatoires comparées aux clés séquentielles.
Les ULIDs ont du sens quand vous voulez des IDs pouvant être générés n'importe où et qui se trient en général par ordre de création. Cela simplifie la pagination par curseur et réduit la douleur des insertions aléatoires qu'on voit souvent avec UUIDv4. Il ne faut toutefois pas considérer l'ULID comme un horodatage parfait ; utilisez created_at quand vous avez besoin d'un ordre strict ou de sécurité lors de backfills.
Oui, particulièrement avec des UUIDv4 aléatoires sur des tables à forte écriture. Les inserts aléatoires se répartissent dans l'index primaire, provoquant plus de splits de pages, plus de churn dans le cache et des index qui grossissent avec le temps. Vous le remarquerez souvent d'abord par des insertions soutenues plus lentes et des besoins mémoire/IO accrus plutôt que par des recherches d'une seule ligne lentes.
Trier par un ID aléatoire (comme UUIDv4) ne correspondra pas au temps de création, donc les curseurs du type “après cet id” ne donnent pas une chronologie stable. La solution fiable est de paginer par created_at et d'ajouter l'ID en tie-breaker, par exemple (created_at, id). Si vous voulez paginer uniquement par ID, un ID triable par le temps comme ULID est généralement plus simple.
Les IDs séquentiels se collident entre shards car chaque shard génère 1, 2, 3... indépendamment. On peut éviter les collisions avec de la coordination (plages par shard ou service d'ID), mais cela ajoute de la complexité opérationnelle et peut devenir un goulot d'étranglement. Les UUIDs/ULIDs réduisent le besoin de coordination car chaque shard peut générer des IDs en toute sécurité.
Les UUIDs/ULIDs sont les plus simples pour exporter/importer et fusionner : vous pouvez transférer des lignes, les importer ailleurs et conserver les références sans renumérotation. Avec des IDs séquentiels, les imports partiels exigent souvent une table de traduction (old_id -> new_id) et une réécriture attentive des clés étrangères, ce qui est source d'erreurs. Si vous clonez souvent des environnements ou fusionnez des jeux de données, les IDs globalement uniques font gagner du temps.
Un modèle courant est d'utiliser deux IDs : une clé primaire interne compacte (serial bigint) pour l'efficacité des jointures et du stockage, plus un ID public immuable (ULID ou UUID) pour les URLs, APIs, exports et références inter-systèmes. Cela garde la base rapide tout en simplifiant les intégrations et migrations. L'important est de considérer l'ID public comme stable et de ne jamais le recycler ou le réinterpréter.
Planifiez-le tôt et appliquez-le de façon cohérente aux tables et APIs. Dans Koder.ai, décidez de la stratégie d'ID par défaut en mode planification avant de générer beaucoup de schémas et endpoints, puis utilisez les snapshots/rollback pour valider les changements pendant que le projet est encore petit. La partie la plus difficile n'est pas de créer de nouveaux IDs, mais de mettre à jour les clés étrangères, caches, logs et intégrations externes qui référencent encore les anciens IDs.