63 points par GN⁺ 2025-04-01 | 4 commentaires | Partager sur WhatsApp
  • Un article qui rassemble des patterns pratiques pour utiliser Postgres de manière plus productive et plus sûre
  • Chaque pattern est modeste en soi, mais leur accumulation finit par faire une grande différence

Utiliser des clés primaires UUID

  • Les UUID étant aléatoires, ils ont des inconvénients en matière de tri et de performance des index
  • Ils occupent plus d’espace que des ID numériques
  • Mais ils offrent les avantages suivants
    • Il est possible de générer un UUID sans se connecter à la base de données
    • Ils peuvent être exposés à l’extérieur en toute sécurité
  • On peut générer automatiquement un UUID comme clé primaire avec gen_random_uuid()

Toujours ajouter les champs created_at et updated_at

  • Lors du débogage, il est très utile de savoir quand un enregistrement a été créé et modifié
  • updated_at peut être configuré pour être mis à jour automatiquement via un trigger
  • La fonction n’a besoin d’être créée qu’une seule fois, mais le trigger doit être appliqué à chaque table

Définir on update/delete restrict sur les clés étrangères

  • Lors de la définition des contraintes de clé étrangère, il faut impérativement utiliser on update restrict on delete restrict
  • Cela évite qu’une suppression de données ne déclenche accidentellement des suppressions en cascade
  • Le stockage coûte peu cher, mais la récupération des données est très difficile ; il vaut donc mieux rester prudent

Recommander l’usage des schémas

  • Le schéma par défaut est public, mais à mesure que l’application grossit, il vaut mieux la séparer dans des schémas distincts
  • Les schémas fonctionnent comme des espaces de noms, et il est possible de faire des jointures entre différents schémas
  • Plus le nombre de tables augmente, plus l’usage des schémas améliore la lisibilité et la maintenabilité

Utiliser le pattern des tables d’enum

  • Plutôt que le type enum de PostgreSQL ou une check constraint, utiliser des tables d’enum est plus flexible
  • Si les valeurs d’enum sont gérées dans une table séparée, on peut ajouter des métadonnées ou étendre facilement les valeurs d’enum
  • Les contraintes sont conservées en référant aux valeurs de la table d’enum via une clé étrangère

Nommer les tables au singulier

  • Il est préférable de nommer les tables au singulier plutôt qu’au pluriel
  • Lors de l’écriture des requêtes, le singulier est plus clair, tandis que le pluriel peut créer des ambiguïtés de sens ou de possession

Nommer les tables de jointure de manière mécanique

  • Pour les relations many-to-many, il est plus sûr et plus clair de nommer les tables de jointure en concaténant les deux noms de table
  • Exemple : person_pet
  • Ajouter un index unique sur la combinaison pour éviter les doublons

Utiliser le soft delete plutôt que la suppression

  • Au lieu de supprimer réellement les données, il est préférable d’utiliser un champ timestamp comme revoked_at pour indiquer le moment de la suppression
  • Cela permet de savoir non seulement si l’élément a été supprimé, mais aussi quand il l’a été
  • Un timestamp fournit plus d’informations qu’une valeur booléenne

Représenter les statuts avec une table de logs

  • Au lieu de représenter un statut dans une seule colonne, stocker l’historique des changements de statut dans une table séparée
  • Le moment où le statut survient est indiqué explicitement dans la colonne valid_at
  • Pour récupérer rapidement le statut le plus récent, on met en place un flag latest ainsi qu’un index unique et un trigger
  • C’est particulièrement avantageux dans le traitement d’événements asynchrones ou dans les situations où l’ordre peut être mélangé

Ajouter un system_id pour les lignes spéciales

  • En plus des tables d’enum, il peut être nécessaire d’avoir certaines « lignes système »
  • Ajouter un champ texte system_id nullable et définir un index unique
  • system_id permet de retrouver clairement certaines lignes spécifiques

Utiliser les vues avec parcimonie

  • Les vues sont utiles pour abstraire des requêtes complexes, mais elles sont difficiles à maintenir
    • Si une colonne est supprimée, il faut recréer la vue
    • Créer des vues sur d’autres vues pose des problèmes de performance et de lisibilité
  • Il faut donc les utiliser avec discernement, uniquement lorsque c’est nécessaire

Tirer activement parti des requêtes JSON

  • Postgres est très puissant non seulement pour stocker du JSON, mais aussi pour renvoyer des requêtes au format JSON
  • Il peut renvoyer des relations imbriquées sous forme de JSON en une seule requête
  • Cela permet de récupérer toutes les données nécessaires d’un coup, sans problème de N+1
  • Inconvénients : perte des informations de type, et nécessité de charger l’ensemble des données en mémoire d’un seul coup
  • Les avantages en termes de performance ou de structure sont toutefois plus importants

4 commentaires

 
jhj0517 2025-04-01

> Nommer les tables de jointure de manière systématique

Je trouve que le simple fait d’avoir une règle de nommage comme celle-ci, c’est déjà une bonne chose~

 
halfenif 2025-04-01

Si l’on envisage UUID7, cela ne permettrait-il pas un tri chronologique ?

 
winterjung 2025-04-01
 
t7vonn 2025-04-01

La méthode consistant à ajouter un timestamp lors d’un soft delete est bonne. Si on utilise un UUID comme clé primaire, on ne peut pas trier par ordre chronologique, donc utiliser un snowflake id ou un ulid semble aussi être une bonne option. Dans ce cas, chaque serveur doit toutefois disposer d’un sequence number.