10 points par GN⁺ 2025-12-16 | 1 commentaires | Partager sur WhatsApp
  • Les UUID v4 sont très aléatoires, ce qui entraîne une inefficacité des index et des E/S excessives ; utilisés comme clés primaires dans PostgreSQL, ils provoquent donc une baisse de performances
  • À cause des insertions aléatoires, les scissions de pages (page splits) et la fragmentation des index deviennent fréquentes, ce qui augmente la taille des journaux WAL et la latence en écriture
  • Les UUID ont une taille de 16 octets, soit deux fois plus qu’un bigint, ce qui réduit le taux de réussite du cache et entraîne un gaspillage de mémoire
  • Ils sont souvent pris à tort pour des identifiants de sécurité, mais selon la RFC 4122, un UUID n’est pas un mécanisme de sécurité destiné à empêcher la devinette
  • Pour une nouvelle base de données, il est recommandé d’utiliser des clés basées sur des séquences entières et, si ce n’est pas possible, des UUID v7 ordonnés dans le temps

Les problèmes de performance des UUID v4

  • Les bases de données PostgreSQL utilisant des clés primaires UUID v4 ont montré de manière constante, ces dix dernières années, une dégradation des performances et des E/S excessives
    • Un UUID v4 est généré avec 122 bits aléatoires, ce qui empêche tout tri efficace dans l’index
    • Lors de l’insertion, il n’est pas stocké sur des pages séquentielles, ce qui provoque des accès aléatoires ; les mises à jour et suppressions exigent elles aussi des parcours inefficaces
  • Les index B-Tree supposent des données triées, mais les UUID v4 n’ont aucun ordre naturel, d’où une faible efficacité à l’insertion
    • Chaque insertion est écrite sur une page arbitraire, ce qui provoque souvent des scissions de pages intermédiaires
    • Il en résulte une latence en écriture plus élevée et une augmentation du WAL

Structure des UUID et alternatives

  • Un UUID est un identifiant de 128 bits (16 octets) stocké dans PostgreSQL sous forme de type uuid binaire
  • UUID v4 repose sur des bits aléatoires, tandis que UUID v7 inclut un horodatage dans les 48 premiers bits, ce qui améliore l’efficacité des index
  • PostgreSQL 18, prévu pour 2025, doit prendre en charge nativement les UUID v7
  • Les UUID v7 peuvent être triés chronologiquement, ce qui améliore la densité des pages et l’efficacité du cache

Pourquoi choisir un UUID, et ses limites

  • Les UUID sont utilisés lorsqu’il faut générer des identifiants sans collision dans des environnements à plusieurs clients ou en microservices
    • Exemple : génération simultanée d’ID sur plusieurs instances de base de données
    Publicité
  • Toutefois, la RFC 4122 précise qu’« il ne faut pas supposer qu’un UUID est difficile à deviner », ce qui en fait un mauvais choix comme identifiant de sécurité
  • La probabilité de collision atteint 50 % après la génération de 2.71×10¹⁸ UUID, donc le risque réel reste faible, mais le coût en performances est élevé

Inefficacité des UUID en espace et en E/S

  • Un UUID occupe deux fois plus d’espace qu’un bigint (8 octets) et quatre fois plus qu’un int (4 octets)
    • Sur de très grandes tables, cela se traduit par une augmentation de l’espace de stockage et des temps de sauvegarde et de restauration plus longs
  • Résultats d’une expérience sur la densité des pages d’index
    • index integer : 97,64 %
    • index UUID v4 : 79,06 %
    • index UUID v7 : 90,09 %
  • Lors d’un test Cybertec, une recherche sur un index UUID v4 a nécessité 8,5 millions d’accès supplémentaires à des pages et montré une hausse des E/S de 31 229 %
    • Dans les mêmes conditions, l’index bigint a nécessité 27 332 accès au buffer, contre 8 562 960 pour l’UUID v4
Publicité

Impact sur le cache et la mémoire

  • En raison de leur distribution aléatoire, les UUID ont un faible taux de réussite du buffer cache (cache hit ratio)
    • Il faut charger davantage de pages en cache, et les pages nécessaires sont fréquemment évincées (eviction)
  • Cette baisse d’efficacité du cache provoque une latence des requêtes et une augmentation de l’utilisation mémoire
  • Pour maintenir les performances, il est recommandé de reconstruire régulièrement les index (REINDEX CONCURRENTLY) ou d’utiliser pg_repack

Mesures d’atténuation des performances

  • Augmenter la mémoire : il est recommandé de disposer d’une RAM égale à 4 fois la taille de la base (ex. : base de 25 Go → 128 Go de mémoire)
  • Ajuster work_mem : allouer davantage de mémoire aux opérations de tri peut améliorer les performances
  • Dans un environnement Rails, le paramètre implicit_order_column permet d’utiliser un champ triable comme created_at à la place d’un UUID
  • La commande CLUSTER permet de réorganiser une table selon un champ triable, mais elle nécessite un verrou exclusif

Recommandation : clés entières et séquences

  • Pour les nouvelles bases de données, il est recommandé d’utiliser des clés basées sur des séquences entières
    • integer (4 octets) permet environ 2 milliards de valeurs, et bigint (8 octets) en offre bien davantage
  • Pour la plupart des applications métier, integer suffit ; pour les services à grande échelle, bigint est plus adapté
  • À la place des UUID v4, des UUID v7 ou l’extension sequential_uuids constituent des alternatives réalistes

Résumé

  • Les UUID v4 entraînent une inefficacité des index due à leur caractère aléatoire, des E/S élevées et une faible efficacité du cache
  • Ils ne peuvent pas servir d’identifiants de sécurité et impliquent un gaspillage d’espace important
  • Les clés de séquence entières conviennent mieux à la plupart des applications
  • Si l’usage d’UUID est inévitable, il faut choisir des UUID v7 ordonnés dans le temps
  • Il faut éviter d’utiliser gen_random_uuid() comme clé primaire dans PostgreSQL

1 commentaires

 
GN⁺ 2025-12-16
Avis sur Hacker News
  • C’est un exemple typique d’optimisation prématurée
    Mettre des données dans un identifiant permanent est un tabou en gestion des données
    Si on met la date de naissance dans un ID, comme avec le numéro d’identité norvégien, on finit par avoir des immigrés dont la date de naissance était erronée, ou des problèmes de pénurie de numéros parce qu’il y a trop de personnes nées le 1er janvier
    À l’époque des catalogues sur fiches, on pouvait comprendre qu’on mélange données et identifiants pour réduire le coût de consultation, mais aujourd’hui, avec des bases de données puissantes, ce n’est plus vraiment nécessaire

    • Cet exemple relève en fait plutôt d’un mauvais choix de valeur par défaut
      Le vrai problème, c’était d’avoir fixé les dates de naissance inconnues au 1er janvier, pas le fait d’avoir mis une date dans la clé
      Avec une valeur non calendaire comme 00 ou 99, il n’y aurait pas eu de collision
      Mettre un timestamp dans un UUID ne sert pas à lui donner du sens, mais à faire une optimisation de performance
      Une clé croissante dans le temps réduit le coût de réécriture du B-tree et améliore les performances d’insertion en base
    • Le code fiscal italien inclut aussi le sexe, ce qui pose problème après une transition de genre
      La règle « ne mettez pas de données dans un identifiant permanent » reste une généralité, mais selon le contexte on peut accepter certains compromis
      Par exemple, utiliser un hash md5 comme UUID pour construire un index crée de la fragmentation, mais à un niveau gérable
    • UUIDv7 n’est qu’un mode de génération avec biais temporel (random bias), il ne contient pas de vraie information métier
      Le choix entre UUID aléatoire et UUID basé sur le temps peut faire gagner des secondes, pas seulement des millisecondes
    • Sur une petite base, c’est de l’optimisation prématurée, mais à grande échelle il faut parfois raisonner à l’inverse
      Sur une grosse base, le sharding et la distribution deviennent indispensables, et dans ce contexte les UUID fonctionnent mieux que l’auto-incrément
    • À propos de l’exemple du numéro norvégien, certains doutent qu’il puisse réellement y avoir autant de gens nés le 1er janvier
      Si le format est DDMMYYXXXXX, cela couvre jusqu’à 100 000 personnes ; difficile de croire qu’on atteigne ce niveau
      Ce serait sans doute un cas très particulier, comme un afflux massif de réfugiés sur une année donnée
  • Il ne faut pas utiliser un UUID comme jeton de sécurité
    S’appuyer sur le fait qu’il est difficile à deviner pour en faire un mécanisme de sécurité est risqué
    L’intérêt d’une valeur aléatoire n’est pas seulement d’empêcher la prédiction, mais aussi de masquer la relation entre des identifiants consécutifs

  • La stratégie de PK change complètement selon le type de base
    Dans Postgres, une PK aléatoire est inefficace, mais dans des bases distribuées comme Cockroach ou Spanner, une clé monotone crée au contraire des hot shards

    • Même dans une base distribuée, une clé à tendance croissante est préférable à du totalement aléatoire
      UUIDv7 combine des bits de poids fort triables et des bits de poids faible aléatoires, ce qui donne à la fois une bonne répartition entre nœuds et une bonne efficacité de stockage local
    • Il vaut mieux voir cela comme une différence de structure de base de données que de type de base
      Dans une base classique non shardée, des clés aléatoires provoquent la fragmentation du B-tree
    • Dans Google Cloud Bigtable, on utilise parfois des clés séquentielles en ordre inversé (reverse) pour déclencher la répartition automatique
    • Dans un Postgres shardé, une PK aléatoire peut être avantageuse
      Mais si les requêtes par plage (range query) sont fréquentes, des clés aléatoires deviennent défavorables
      Au final, il faut choisir selon les caractéristiques de la charge de travail
    • Pour une charge dominée par l’écriture avec un fort biais temporel, une PK aléatoire peut aussi être meilleure dans Postgres
  • L’article pointe bien les inconvénients d’UUIDv4 comme PK, mais la méthode d’obfuscation d’entiers proposée semble peu adaptée à un service en production
    Pour une petite base, UUIDv7 est un compromis raisonnable

    • Personnellement, je préfère UUIDv4 à UUIDv7
      Je ne veux pas exposer l’instant de génération
      Tant que le volume de données n’est pas assez grand pour que le caractère aléatoire d’UUIDv4 devienne un problème de performance, v4 est un choix plus sûr
    • Dans Postgres, j’aime utiliser une séquence unique
      Il y a une petite fuite d’information, mais c’est suffisamment opaque en pratique
    • Si l’objectif est simplement de cacher le nombre d’utilisateurs, on peut appliquer une permutation cryptographique à une clé auto-incrémentée
      Par exemple, la transformer avec AES-128 puis l’encoder en base64 pour obtenir quelque chose qui ressemble à un identifiant de vidéo YouTube
  • Dans les audits techniques, je vois beaucoup d’entreprises pour lesquelles la capacité à sharder rapidement est un facteur clé de croissance
    Mettre des UUID dans toutes les tables permet d’étendre l’architecture au moment du sharding sans modifier la structure
    Le gain en scalabilité est bien supérieur au léger coût en espace et en temps

    • UUIDv7 offre aussi un avantage de performance dans Postgres grâce à son caractère monotone
    • Nous aussi avons souffert de ne pas avoir de UUID pendant un processus de sharding
      Au final, le modèle de données était tellement complexe que la migration elle-même était difficile, avec ou sans UUID
  • Dans notre application, nous chiffrons les PK entières pour qu’elles ressemblent à des UUID
    Sinon, l’exposition d’identifiants séquentiels permet d’estimer le nombre de clients ou de lancer des attaques par dictionnaire
    Les identifiants chiffrés permettent de détecter immédiatement les tentatives de scan grâce aux échecs de déchiffrement

    • Mais cela peut poser un problème d’impossibilité de déchiffrement en cas de perte ou de rotation de clé
    • Je me demande comment les clés sont gérées — injectées via des variables d’environnement, embarquées dans le code, usage d’un schéma AEAD comme AES-GCM, etc. ; la gestion de la sécurité est essentielle
  • Dire que « 2 milliards, c’est largement suffisant » est dangereux
    Tous les DBA ont au moins une histoire cauchemardesque née de ce type de décision

  • L’article dit que « les valeurs aléatoires se trient mal », mais en réalité un tri par ordre des octets est tout à fait possible
    En revanche, comme les clés aléatoires ne sont pas insérées séquentiellement, le B-tree se rééquilibre plus souvent et les performances baissent

    • UUIDv4 est utile en environnement distribué, mais il faut accepter le coût d’un espace sur 128 bits et de l’absence de séquentialité
    • L’auteur a ensuite ajouté une comparaison expérimentale d’index B-tree
      Avec des PK entières, l’index tient mieux en mémoire ; avec UUIDv4, les accès aux pages sont plus nombreux, ce qui augmente la latence
    • Certains ont estimé que l’argumentation technique manquait de fondement
    • Plus la clé d’un B-tree est croissante, plus les insertions sont efficaces, tandis que des clés aléatoires sont moins cache-friendly
    • Plus l’accès aux données est proche du moment de création, plus un ordre chronologique est avantageux en performance
  • Cet article donne l’impression d’une optimisation prématurée où la solution précède le problème
    UUIDv4 est parfaitement correct dans la plupart des cas
    Les problèmes de performance ne devraient être pris en compte que lorsqu’ils se manifestent réellement

    • Mais une fois qu’on démarre avec UUIDv4, il devient presque impossible de rekeyer plus tard en int64
    • En pratique, quand les problèmes de performance apparaissent, on est déjà en phase de croissance et on n’a plus la marge pour changer de PK
  • En résumé, dans Postgres, UUIDv7 offre des performances légèrement meilleures que v4
    Sur les versions récentes, UUIDv7 est pris en charge sans plugin

    • Mais le message principal de l’article reste : utilisez si possible une PK entière avec séquence
    • À partir de Postgres 18, il existe une fonction intégrée uuidv7(), mais on ne sait pas encore clairement si les extensions restent plus riches en fonctionnalités
    • La plupart des utilisateurs n’auront désormais plus besoin d’extension séparée