Le choix de la clé primaire est la décision la plus banale d’une modélisation, et dont on hérite pendant des années.
Une colonne status mal nommée se renomme un dimanche soir. Une clé primaire, elle, se propage dans toutes les clés étrangères, tous les index, tous les caches, potentiellement toutes les URLs publiques, les exports, et dans le code de tous les services qui parlent à cette base.
Elle se change rarement sans une opération de migration à part entière.
Dans les années 80, 90 et jusqu’au début des années 2000, le débat n’existait pas 👉 on utilisait des identifiants séquentiels, point.
Tout a changé à la fin des années 2000. Le web at scale, les volumes de données qui vont avec et le big data ont commencé à remettre en cause ces identifiants, au profit de formes qu’on peut générer sans passer par un organe central (la séquence) côté client ou sur N serveurs à la fois.
Les premiers que j’ai vus publier sur le sujet sont les équipes de Twitter, avec leur Snowflake ID et l’implémentation open source qui allait avec, aujourd’hui archivée.
Depuis, l’UUID est entré dans les mœurs, au point d’être devenu le choix par défaut, souvent sans qu’on prenne le recul de vérifier si c’est vraiment le bon.
Deux camps se sont formés 👇
- D’un côté, ceux qui y voyaient la condition d’applications capables de scaler : l’identifiant se génère côté client, en quantité illimitée, sans jamais rien demander à la base.
- De l’autre, les plus réservés, qui rappelaient que l’UUIDv4, largement majoritaire, évite certes les collisions, mais que son caractère purement aléatoire disperse les insertions sur tout l’index au lieu de les concentrer tout en multipliant les découpages de pages et alourdissant un index déjà deux fois plus gros qu’avec un BIGINT.
Et l’argument qui tranchait le débat vient de tomber : depuis la version 18, sortie en septembre 2025, PostgreSQL génère nativement des UUID ordonnés dans le temps (uuidv7()) 👉 « Les UUID détruisent vos index » n’est plus une réponse suffisante.
Cet article compare BIGINT séquentiel, UUIDv4, UUIDv7, ULID et TSID sur des mesures réelles plutôt que sur des impressions, et montre surtout dans quelles conditions chacun est le bon choix, car aucun ne l’est partout.
1. Ce qu’une clé primaire coûte vraiment
Avant de comparer, il faut savoir où se paie la facture. Une clé primaire dans PostgreSQL, c’est trois choses :
- Une colonne dans la table (le heap). 8 octets pour un
bigint, 16 pour unuuid, 37 pour un UUID stocké bêtement entext. - Un index B-tree unique, créé automatiquement. C’est lui qui souffre le plus du choix.
- Une copie de la valeur dans chaque clé étrangère qui pointe vers la table, plus l’index sur cette clé étrangère. C’est le multiplicateur : une table référencée par cinq autres paie six fois la différence de taille.
Spécificité PostgreSQL
Beaucoup d'articles sur le sujet (souvent écrits pour MySQL/InnoDB) expliquent qu'une grosse clé primaire gonfle tous les index secondaires, parce que la table est un index clusterisé et que chaque index secondaire embarque la clé primaire dans ses feuilles.Ce n'est pas le cas de PostgreSQL : les tables sont des heaps, et un index secondaire référence un
ctid (6 octets) et non la clé primaire. Une clé primaire large est donc moins pénalisante sur PostgreSQL qu'ailleurs bien que la facture des clés étrangères, elle, reste entière.
Le deuxième coût n’apparaît sur aucun schéma : c’est celui de l’ordre d’insertion.
Un index B-tree range ses valeurs triées, réparties sur des pages de 8 Ko.
Insérer une ligne, c’est donc écrire dans la page qui correspond à sa clé. Et là, tout dépend de la clé.
Une clé croissante, comme une séquence ou un timestamp, vise toujours la même page : la dernière de l’index.
Elle reste en cache en permanence, on la remplit jusqu’au bout, puis on en ouvre une nouvelle. PostgreSQL reconnaît d’ailleurs ce cas : quand une page se remplit à l’extrémité droite de l’arbre, il ne la coupe pas en deux moitiés égales, il la laisse pleine à 90 % (valeur par défaut du fillfactor) et démarre la suivante à vide. Le résultat est un index compact, dont on ne touche jamais que la queue.
Une clé aléatoire, elle, vise une page différente à chaque insertion, tirée au hasard parmi toutes celles de l’index.
Trois factures s’additionnent :
- La lecture.
Il faut aller chercher cette page. Tant que l’index tient en RAM, ça passe. Dès qu’il la dépasse, c’est un accès disque par insertion. - Le WAL.
Après chaque checkpoint, la première écriture sur une page la fait journaliser en entier : 8 Ko de WAL pour insérer 20 octets. En séquentiel, on paie ce prix une fois pour mille lignes ; en aléatoire, à peu près à chaque ligne. - L’espace.
Quand une page pleine doit être coupée, PostgreSQL choisit où couper. À l’extrémité droite de l’arbre, il sait que la suite arrivera à droite : il laisse la page de gauche pleine. Ailleurs, il ne sait rien de la suite et coupe au milieu. Chaque page oscille alors entre 50 % après une coupure et 100 % avant la suivante, ce qui donne un index rempli à ~70 % en moyenne, contre ~90 % en séquentiel. Soit 30 % de pages en plus pour les mêmes données et un index qui franchit d’autant plus tôt le seuil de la RAM, ce qui ramène au premier point.
Plus de détails
Pourquoi 50/50 quand c'est aléatoire.
Quand une page est pleine et qu'il faut y insérer, PostgreSQL la coupe en deux et répartit son contenu. Reste à choisir où couper. Sur la page la plus à droite, il sait où tomberont les prochaines insertions : toutes à droite. Il a donc intérêt à laisser la page de gauche pleine car elle ne recevra plus rien, autant ne pas y gaspiller d'espace. Ailleurs dans l'arbre, il n'a aucune idée d'où tombera la suite. Couper au milieu est le choix qui maximise la place disponible des deux côtés. C'est un pari sur l'ignorance, pas un défaut.
Pourquoi ~70 % à l'équilibre.
Une page qui vient d'être coupée est à 50 %. Elle se remplit ensuite jusqu'à 100 %, puis se recoupe et retombe à 50 %. Chaque page oscille donc en permanence entre ces deux bornes. À un instant donné, l'index contient un mélange de pages à tous les stades de ce cycle. La moyenne n'est pas 75 %, mais plutôt ln(2), soit 69,3 %, un résultat classique sur les B-trees où les pages passent plus de temps dans le bas du cycle que dans le haut, parce qu'une page presque vide reçoit statistiquement moins d'insertions qu'une page presque pleine. D'où le « ~70 % », qui n'est ni un réglage ni un plafond, mais un point d'équilibre.
30% ❓
Pour stocker la même quantité de données, il faut 0,90 / 0,70 ≈ 1,29 fois plus de pages. Donc 30% de plus arrondit vers le haut.
La boucle est bouclée.
Le premier point indique : si l'index dépasse la RAM, chaque insertion coûte un accès disque.
Le troisième dit : l'aléatoire fait grossir l'index de 30 %.
👉 Les deux se relient, car un index plus gros franchit plus tôt le seuil de la RAM, et une fois ce seuil franchi, le coût par insertion change de nature. La latence disque, jusque-là absente du chemin d'insertion, s'y invite à chaque ligne.
2. La mesure
Plutôt que de répéter les intuitions du web, voici un test reproductible :
quatre tables identiques, deux millions de lignes chacune, sur PostgreSQL 18.4 (conteneur Docker, paramètres par défaut).
On démarre un container:
docker container run --rm -it --name pg18 -e POSTGRES_PASSWORD=postgres postgres:18.4
Dans un second shell :
docker container exec -it pg18 psql -h localhost -U postgres
CREATE TABLE t_bigint (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, label TEXT NOT NULL);
CREATE TABLE t_uuidv4 (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), label TEXT NOT NULL);
CREATE TABLE t_uuidv7 (id UUID PRIMARY KEY DEFAULT uuidv7(), label TEXT NOT NULL);
CREATE TABLE t_uuidtxt (id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, label TEXT NOT NULL);
INSERT INTO t_bigint (label) SELECT 'row-' || g FROM generate_series(1, 2000000) g;
INSERT INTO t_uuidv4 (label) SELECT 'row-' || g FROM generate_series(1, 2000000) g;
INSERT INTO t_uuidv7 (label) SELECT 'row-' || g FROM generate_series(1, 2000000) g;
INSERT INTO t_uuidtxt (label) SELECT 'row-' || g FROM generate_series(1, 2000000) g;
Les tailles se lisent avec pg_relation_size, le WAL produit avec un pg_current_wal_lsn() avant/après, et la densité des feuilles d’index avec l’extension pgstattuple :
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT idx.relname, avg_leaf_density, leaf_fragmentation
FROM pg_class idx
JOIN LATERAL pgstatindex(idx.oid::regclass) ON TRUE
WHERE idx.relname LIKE 't\_%\_pkey';
Résultats :
| Clé primaire | Insert 2M | Table | Index PK | WAL généré | Densité feuilles | Fragmentation |
|---|---|---|---|---|---|---|
BIGINT identity |
1,19 s | 100 Mo | 43 Mo | 285 Mo | 90,0 % | 0 % |
UUID v7 |
1,73 s | 115 Mo | 60 Mo | 312 Mo | 90,0 % | 0 % |
UUID v4 |
2,97 s | 115 Mo | 76 Mo | 377 Mo | 71,7 % | 50,1 % |
TEXT (UUID en chaîne) |
3,99 s | 154 Mo | 146 Mo | 470 Mo | 69,5 % | 49,9 % |
Trois enseignements :
- L’entier séquentiel gagne partout, mais l’écart avec UUIDv7 est modeste : +40 % sur l’index, +10 % sur le WAL. C’est le prix de 8 octets supplémentaires par ligne, pas une catastrophe.
- L’aléatoire coûte cher, et il coûte deux fois. UUIDv4 paie les 16 octets et la fragmentation : son index est 27 % plus gros que celui d’UUIDv7 pour exactement les mêmes données, avec la moitié des feuilles fragmentées. La colonne « densité » explique le reste du tableau à elle seule.
- Stocker un UUID en
textouvarchar(36)est l’erreur la plus coûteuse du lot : index 2,4× plus gros qu’enuuid, insertions 2,3× plus lentes. Le typeuuidde PostgreSQL est un binaire de 16 octets, utilisez-le.
Honnêteté sur le protocole
Ces chiffres sous-estiment le problème de l'aléatoire. Ici l'index tient entièrement en mémoire et l'insertion est unINSERT ... SELECT en masse, dans une transaction unique. En production, avec un index de plusieurs gigaoctets et des insertions unitaires concurrentes, chaque insertion aléatoire devient une lecture disque : l'écart entre v4 et v7 se creuse nettement, tandis que l'écart entre bigint et UUIDv7 reste, lui, à peu près constant.
3. L’entier séquentiel
C’est le défaut historique, et il reste excellent.
CREATE TABLE invoices (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
...
);
GENERATED ALWAYS AS IDENTITY vs BIGSERIAL ❓
GENERATED ALWAYS AS IDENTITY est la forme standard SQL depuis PostgreSQL 10.
Elle interdit l'écriture manuelle d'un `id`, ce qui empêche la désynchronisation de la séquence.
BIGSERIAL n'est qu'un DEFAULT nextval(...), donc contournable et un jour nextval renvoie une valeur déjà prise.
Sur beaucoup de projets, les clés primaires sont encore des INT. En soi, ce n’est pas dramatique : tout dépend de la volumétrie et de la charge réelles, et la grande majorité des applications ne sont ni Netflix ni Google. Le piège est ailleurs. Dès qu’une table sert à historiser ou à journaliser une entité qui subit beaucoup de mises à jour, les 2,1 milliards arrivent bien plus vite qu’on ne l’imagine.
C’est exactement ce qui est arrivé chez mon client Stonal : il a fallu basculer d’INT à BIGINT.
La migration s’est bien passée, mais elle a été longue.
- Création d’une nouvelle colonne
_id. - Backfill des nouveaux identifiants, pour repartir d’une séquence cohérente.
- Création puis backfill des colonnes correspondantes dans chaque clé étrangère.
- Et l’étape la plus risquée : le switch, avec désactivation des contraintes, renommage des colonnes, réactivation des contraintes.
Vous l’aurez compris : la migration s’est étalée sur plusieurs livraisons de scripts. La morale tient en une ligne :
Quand vous créez une table avec un identifiant séquentiel, à moins d’être certain qu’elle ne grossira jamais (une table de référentiel, typiquement), prenez un
BIGINT.
Ses vraies limites, celles qui poussent à chercher autre chose :
- La coordination.
La valeur vient de la base. L’application ne connaît son identifiant qu’après l’INSERT(RETURNING id). Écrire un graphe d’objets liés en un seul aller-retour devient pénible, et l’écriture offline-first impossible. - La fusion de données.
Deux bases avec les mêmes plages d’idne se fusionnent pas sans réécrire toutes les clés étrangères. Même problème pour du sharding, ou pour restaurer un sous-ensemble de lignes depuis un backup dans une base vive. - La fuite d’information.
/invoices/1042dit à un concurrent que vous avez émis 1042 factures, et deux commandes passées à une semaine d’intervalle révèlent votre volume hebdomadaire. C’est le problème des chars allemands, appliqué à votre chiffre d’affaires. - L’énumération.
Un identifiant devinable transforme le moindre oubli de contrôle d’accès en fuite de masse : il suffit d’incrémenter. Ce n’est pas un défaut de l’entier car l’autorisation reste la vraie réponse mais c’est un facteur d’amplification bien réel.
Un mythe à corriger
Une séquence PostgreSQL n'est pas transactionnelle : unROLLBACK ne rend pas le numéro consommé. Vos id auront donc des trous, et c'est normal. Ne construisez jamais une numérotation métier continue (numéros de facture, mentions légales) sur une séquence : il faut une table de compteurs verrouillée, ou un calcul a posteriori.
4. L’UUID v4 : le piège de l’aléatoire
gen_random_uuid() produit un UUID version 4 : 122 bits aléatoires. Unique partout, sans coordination, imprévisible. Et c’est précisément l’aléatoire qui pose problème quand la valeur devient une clé d’index.
Le tableau de la section 2 le montre : +27 % d’index, +21 % de WAL, la moitié des feuilles fragmentées par rapport à un UUID trié. Et le pire est ailleurs :
- La localité des écritures disparaît. Avec une clé croissante, les lignes écrites au même moment vivent dans les mêmes pages : un
checkpointréécrit peu de pages, le cache est efficace. Avec de l’aléatoire, N insertions touchent N pages différentes, dispersées dans l’index. - Les full-page images se multiplient. Après un checkpoint, la première modification d’une page journalise la page entière (8 Ko) dans le WAL. Toucher beaucoup de pages distinctes, c’est beaucoup de FPI — c’est l’origine principale du surcoût de WAL mesuré.
- La localité des lectures aussi. « Les 100 dernières commandes » est un
Index Scanséquentiel avec une clé triée, et 100 accès dispersés avec un UUIDv4.
En contrepartie, UUIDv4 offre une propriété que les identifiants triés n’ont pas : il ne révèle rien. Pas de date de création, pas d’ordre, pas de volume. Pour un jeton de partage public, une URL de réinitialisation, une ressource dont l’existence même est une information, c’est le bon choix mais ce sont des jetons, pas nécessairement des clés primaires.
Un UUIDv4 par ligne, ce n’est pas un problème. Un UUIDv4 comme clé de tri d’un index de 200 Go alimenté à 5 000 insertions par seconde, c’en est un.
5. Les identifiants triés dans le temps
L’idée est simple : mettre l’horodatage dans les bits de poids fort, et l’aléatoire dans les bits de poids faible.
On obtient un identifiant globalement unique, générable côté client, et croissant donc compatible avec l’optimisation de page à droite du B-tree.
Trois implémentations dominent.
UUIDv7 — le nouveau défaut
Standardisé par la RFC 9562 (2024) qui déprécie l’ancienne standardisation de l’UUID, la RFC 4122, UUIDv7 est un UUID de 128 bits dont les 48 premiers bits sont un timestamp Unix en millisecondes, le reste étant de l’aléatoire (PostgreSQL y place en plus 12 bits de fraction de milliseconde, ce qui rend les identifiants créés dans la même milliseconde ordonnés entre eux).
Depuis PostgreSQL 18, c’est une fonction native donc plus besoin d’extension ni de génération applicative :
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT uuidv7(),
...
);
SELECT uuid_extract_timestamp(id), uuid_extract_version(id) FROM orders LIMIT 1;
-- 2026-08-02 21:12:50.551+00 | 7
C’est un uuid standard : mêmes 16 octets, même type, même sérialisation. Tout ce qui sait lire un UUID sait lire un UUIDv7, sans changement de driver ni d’ORM. Sur une base antérieure à la 18, l’extension pg_uuidv7 fournit la même fonction.
La génération peut aussi se faire côté applicatif :
id, err := uuid.NewV7() // github.com/google/uuid
let id = Uuid::now_v7(); // crate uuid, feature "v7"
UUID id = UuidCreator.getTimeOrderedEpoch(); // com.github.f4b6a3:uuid-creator
ULID — la même idée, un autre encodage
Un ULID, ce sont 128 bits également : 48 bits de timestamp et 80 bits d’aléatoire, mais encodés en Crockford base32 sur 26 caractères (01ARZ3NDEKTSV4RRFFQ69G5FAV) — plus courts et plus lisibles qu’un UUID canonique, sans tiret, sans caractère ambigu, sûrs dans une URL.
Techniquement, ULID et UUIDv7 résolvent le même problème avec le même layout.
La différence est un encodage textuel, pas une structure.
Le piège est là : PostgreSQL n’a pas de type ULID, et la tentation est de créer une colonne text de 26 caractères. La dernière ligne du tableau de la section 2 dit ce que ça coûte.
Un ULID fait 128 bits :
- stockez-le dans une colonne
uuid(la conversion base32 ↔ hexadécimal est bijective) , - ne faites du base32 que dans la couche de présentation.
Autrement dit :
si vous vouliez un ULID, prenez un UUIDv7 stocké en
uuid, et affichez-le comme vous voulez.
Vous gardez le type natif, les index, les outils, et le support de tous les drivers.
TSID — quand 64 bits suffisent
Le TSID (Time-Sorted Identifier, popularisé par Vlad Mihalcea) prend le contre-pied : 64 bits seulement avec 42 bits de timestamp en millisecondes (~139 ans) et 22 bits partagés entre identifiant de nœud et compteur aléatoire.
L’intérêt : il tient dans un bigint. On garde :
- la génération côté client,
- l’ordre temporel,
- l’absence de coordination avec la base ,
- le coût de stockage d’un entier séquentiel.
C’est le meilleur ratio du lot.
Le prix à payer: l’unicité n’est plus statistique mais configurée.
22 bits par milliseconde, ce n’est pas infini ; il faut attribuer un identifiant de nœud distinct à chaque générateur (variable d’environnement, index de StatefulSet, ID d’instance). Un déploiement où deux pods partagent le même nœud logique produit des collisions, et une collision sur une clé primaire, c’est une erreur en production.
👉 C’est un vrai coût opérationnel, à peser contre 8 octets par ligne.
6. Le compromis hybride : clé interne, identifiant public
Il existe une troisième voie, souvent la plus pragmatique sur une base existante :
garder un
bigintcomme clé primaire technique et exposer un identifiant opaque.
Aparté : clé de substitution et clé naturelle
Une clé de substitution (surrogate key) est un identifiant sans aucun sens métier, inventé pour désigner une ligne et rien d'autre : un bigint auto-incrémenté, un UUID, un TSID. Elle s'oppose à la clé naturelle (business key), une donnée qui identifie déjà l'objet dans le monde réel : un email, un SIRET, un ISBN, un numéro de commande.
L'intérêt de la première, c'est qu'elle ne change jamais. Une clé naturelle, si : un email se corrige, un SIRET se réattribue, un numéro de commande change de format le jour où le service commercial refait sa nomenclature. Et une clé primaire qui change doit être propagée dans chaque table qui la référence. D'où la règle de la section 7 : clé de substitution en PRIMARY KEY, clé métier en UNIQUE.
Attention à ne pas confondre cet axe avec celui de cette section. Ici, id et public_id sont tous les deux des clés de substitution : aucun des deux ne porte de sens métier. Ce qui les distingue n'est pas leur nature mais leur exposition, l'un ne sortant jamais de la base quand l'autre est fait pour être publié. Ce second pattern n'a pas de nom canonique ; on le croise sous « public ID », « external ID » ou « identifiant opaque » — les cus_... de Stripe, ou le node_id de GitHub à côté de son id numérique.
CREATE TABLE customers (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
public_id UUID NOT NULL DEFAULT uuidv7(),
...
);
CREATE UNIQUE INDEX customers_public_id_key ON customers (public_id);
Les clés étrangères, les jointures et les index internes utilisent id (8 octets, compact, rapide). Les URLs, les APIs, les webhooks et les exports utilisent public_id. Aucune information ne fuit, et le cœur de la base garde ses performances.
Ce que ça coûte, honnêtement :
- Un index unique supplémentaire à maintenir sur chaque table concernée, sur une colonne aléatoire ou semi-aléatoire.
- Une indirection permanente : chaque requête entrante résout d’abord
public_id → id. C’est un accès index, donc rapide, mais c’est du code à écrire partout et une source d’erreurs (le jour où unidinterne finit dans une réponse d’API, la fuite est silencieuse). - Deux identifiants pour un même objet dans les logs, les tickets de support et les conversations. Le coût est humain plus que technique, mais il est réel.
C’est un bon choix quand la base existe déjà en bigint et qu’on veut arrêter d’exposer des identifiants énumérables.
C’est un choix plus discutable sur un schéma neuf, où uuidv7() en clé primaire donne le même bénéfice sans indirection.
Si je vous en parle, c’est parce que c’est la solution que nous avons choisie chez mon client, Stonal 🫠
A une différence près, l’id public est un UUID v4.
7. Choisir
| Besoin | Choix |
|---|---|
| Monolithe, base unique, identifiants non exposés | BIGINT identity |
| Schéma neuf, API publique, microservices | UUID + uuidv7() |
| Génération côté client indispensable, volumétrie énorme | TSID en bigint |
Base bigint existante à ne pas migrer |
bigint + public_id UUID |
| Jeton public, non énumérable, non datable | UUID + gen_random_uuid() (v4) |
Et quelques règles qui ne dépendent pas du contexte :
- Éviter un UUID en
text/varchar(36)/char(36). Le typeuuidexiste, il fait 16 octets contre 37, il compare en binaire. - Éviter l’UUIDv4 comme clé primaire d’une table à forte volumétrie d’écriture. UUIDv7 offre les mêmes garanties sans la fragmentation.
- Éviter les clés naturelles (email, SIRET, numéro de commande) comme clé primaire. Les données métier changent (un email se corrige, un SIRET se réattribue) et une clé primaire qui change doit être propagée dans toutes les tables qui la référencent. Clé technique en
PRIMARY KEY, clé métier enUNIQUE. - Privilégier
GENERATED ALWAYS AS IDENTITYàSERIAL/BIGSERIALsur un schéma neuf. Cela fait la même chose sans le piège de la séquence désynchronisée. - Attention à ce que révèle un identifiant trié. UUIDv7 contient sa date de création en clair (
uuid_extract_timestample prouve) et permet de deviner l’ordre de création. Ce n’est presque jamais un problème sauf quand la date de création est elle-même l’information sensible.
⚠️ Migrer une clé primaire
Passer debigint à uuid sur une table en production n'est pas un ALTER TABLE. C'est un chantier de plusieurs semaines sur un schéma mature, d'où l'intérêt de choisir correctement au début, ou d'assumer l'approche hybride de la section 6.
En résumé
- La clé primaire coûte des octets et un ordre d’insertion. Le second est souvent plus cher que le premier.
- L’aléatoire pur (UUIDv4) fragmente les index.
- UUIDv7 supprime ce surcoût en gardant les propriétés de l’UUID, et il est natif depuis PostgreSQL 18 (
uuidv7()). - ULID et UUIDv7 sont la même idée avec un encodage différent : stockez-les en
uuid, jamais entext. - TSID est l’option 64 bits, la plus compacte, au prix d’une gestion d’identifiants de nœuds.
- Le
bigintséquentiel reste le meilleur choix technique pur, et le pire choix pour un identifiant exposé publiquement.
La bonne question n’est pas « entier ou UUID ? » mais « qui génère l’identifiant, et qui a le droit de le voir ? »
Ressources
- UUID or TSID as Database Primary Key? — Vlad Mihalcea
- UUID vs Sequential ID as Primary Key — Baeldung
- Choosing the Best Primary Key: Integer vs UUID vs ULID
- Why We Stopped Using UUIDs as Primary Keys
- RFC 9562 — Universally Unique IDentifiers (UUIDs)
- PostgreSQL 18 — Notes de version
- Exploring PostgreSQL 18’s new UUIDv7 support — Aiven
- UUIDv7 in PostgreSQL 18: What You Need to Know — DbVisualizer
- Spécification ULID
- Stateless Snowflake: A Cloud-Agnostic Distributed ID Generator Using Network-Derived Identity
- Should We Rethink About IDs? A Deep Dive into “Snowflake IDs”
Mesures réalisées sur PostgreSQL 18.4 (image postgres:18.4), configuration par défaut, 2 000 000 de lignes par table.

