UUID vs ULID vs IDs séquentiels : choisir le bon ID pour votre base de données
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.

Le vrai problème : choisir un ID que vous ne regretterez pas plus tard
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 :
- Les inserts et les requêtes ralentissent parce que l'index de la clé primaire croît selon un schéma hostile.
- La pagination devient compliquée quand le tri par ID ne correspond pas au temps de création.
- Les migrations deviennent risquées une fois que d'autres systèmes dépendent du format de vos IDs.
- Le débogage prend plus de temps quand les IDs sont difficiles à lire ou comparer.
- Les imports entrent en collision lorsque vous fusionnez des données de plusieurs sources.
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.
Définitions rapides en langage simple
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.
IDs séquentiels (integer/bigint)
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.
UUIDs
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.
ULIDs
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 :
- Séquentiel : le plus petit et ordonné par défaut.
- UUID : le plus simple à générer indépendamment, le moins lisible humainement, souvent en ordre aléatoire.
- ULID : indépendant comme UUID, mais approximativement ordonné temporellement.
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 ».
Indexation et performance : ce qui change en pratique
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 :
- serial bigint : 8 octets
- UUID : 16 octets
- ULID : 16 octets si stocké en binaire, beaucoup plus si stocké en tant que chaîne de 26 caractères
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.
Tri, pagination et ordre temporel
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.
Ce que l'ULID vous apporte (et ce qu'il n'apporte pas)
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.
Quand created_at reste préférable
Trier 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.
Sharding plus tard : éviter les collisions d'ID
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 plan simple qui fonctionne (et ce que ça coûte)
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.
Exports et imports de données
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 :
- Assurez-vous que l'importeur ne génère pas de nouveaux IDs par défaut.
- Importez les parents avant les enfants et validez les clés étrangères après le chargement.
- Si vous utilisez des IDs séquentiels, réinitialisez les séquences sinon le prochain insert peut entrer en collision.
- Pour les ULIDs, stockez et exportez-les de façon cohérente (chaîne vs binaire).
Comment choisir en 10 minutes
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_atvous 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
BIGINTsé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.
Erreurs courantes et pièges
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 :
- Utiliser des UUIDs partout sans vérifier le coût. UUIDv4 peut gonfler les index et réduire l'efficacité du cache. L'application fonctionne toujours, mais vous paierez avec des écritures plus lentes et des backups plus volumineux.
- Compter sur des IDs séquentiels, puis devoir fusionner des données entre systèmes, régions ou shards. Les collisions apparaissent lors des imports ou syncs, et des correctifs rapides comme des offsets ou préfixes tendent à s'infiltrer dans toutes les intégrations.
- Penser que l'ULID rend tout plus rapide. Il aide pour l'ordre d'insertion et le tri temporel, mais il ne réparera pas des jointures lentes, des index manquants ou des lignes larges. Certains générateurs ne sont pas strictement monotones sous forte concurrence.
- Exposer des IDs séquentiels publiquement. Si vos URLs ou APIs utilisent
123, 124, 125, les gens peuvent deviner des enregistrements voisins et sonder votre système. - Changer le type d'ID en plein projet sans plan de migration. Les clés étrangères, caches, logs et payloads externes peuvent continuer à référencer les anciens IDs longtemps après votre changement.
Signes d'alerte à traiter tôt :
- Vous attendez des imports de partenaires ou vous fusionnez régulièrement des données entre environnements.
- Vous avez besoin d'une pagination ordonnée dans le temps sans dépendre d'un timestamp séparé.
- Vous prévoyez de partager des IDs en dehors de votre système (URLs, webhooks, applis mobiles).
- Vous ne pouvez pas tolérer une fenêtre d'arrêt pour une migration d'ID massive.
- Vous attendez de très grandes tables où la taille d'index et la vitesse d'écriture importent.
Checklist rapide avant de vous engager
Réalité base de données et requêtes
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.
Vérification pour la future compatibilité
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 exemple réaliste : du MVP au multi-système
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 :
- Garder une clé primaire bigint pour les jointures internes et l'efficacité de stockage.
- Ajouter un ID public immuable séparé (ULID, ou UUIDv7 si disponible) pour le partage, la synchronisation et les imports.
- Utiliser l'ID public dans les fichiers d'export et comme clé de fusion entre systèmes.
Étapes pratiques suivantes pour votre projet
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 :
- Outil interne avec une seule base et peu de risques d'intégration : utilisez une clé primaire
bigintséquentielle. - Appli publique avec URLs partageables ou création côté client : utilisez des UUIDs (difficiles à deviner, sûrs entre systèmes).
- SaaS susceptible d'être sharded par tenant ou région : utilisez des ULIDs (ou UUIDv7) pour que les nouvelles lignes tombent près les unes des autres dans les index.
- Beaucoup d'importations depuis des partenaires et dispositifs hors ligne : évitez les IDs séquentiels purs pour les entités externes.
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é.
FAQ
How do I choose between serial IDs, UUIDs, and ULIDs without overthinking it?
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.
When is a serial bigint primary key the best choice?
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.
When should I use UUIDs as my primary key?
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.
What’s the practical advantage of ULIDs over UUIDs?
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.
Do random UUID primary keys really hurt performance in Postgres?
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.
Why does pagination get weird when I sort by UUID?
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.
How does my ID choice affect sharding later?
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é.
Which ID type is safest for exports, imports, and merging datasets?
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.
Should I use one ID for everything, or have both internal and public IDs?
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.
What’s the safest way to commit to an ID strategy when building with Koder.ai?
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.