22 oct. 2025·8 min de lecture

Démêlez les relations « spaghetti » de votre base de données avec une refonte claire

Démêlez les relations spaghetti d'une base de données en détectant dépendances circulaires, tables surchargées et propriété floue, puis refondez avec des règles simples.

Démêlez les relations « spaghetti » de votre base de données avec une refonte claire

À quoi ressemblent les relations spaghetti en pratique

Les relations « spaghetti » dans une base de données signifient que vos tables sont liées de manière désordonnée et surprenante. Au lieu de quelques chemins clairs (comme users -> orders -> payments), vous obtenez une toile de liens croisés où beaucoup de tables pointent vers d'autres, les règles sont incohérentes et personne ne sait expliquer pourquoi une relation existe. Le schéma fonctionne techniquement, mais il est difficile à comprendre.

On le ressent au quotidien. Un petit changement (ajouter un champ, modifier un statut, séparer une fonctionnalité) se transforme en réaction en chaîne : une migration casse un rapport, une correction pour un bug en crée un autre, et des requêtes simples exigent six jointures plus des filtres étranges pour éviter les doublons. Les gens commencent à copier des requêtes depuis d'anciens tickets parce que les comprendre depuis zéro prend trop de temps.

Signes avant-coureurs (même si vous n'êtes pas expert en bases)

Vous pouvez souvent repérer le désordre sans lire chaque table. Surveillez ces schémas :

  • Le même concept apparaît à plusieurs endroits (par exemple customer_id et buyer_id, ou trois colonnes status différentes).
  • Les tables ont beaucoup de colonnes nullables qui ne s'appliquent qu'à des cas particuliers (une table « tout-en-un").
  • Vous voyez des tables de jointure pour des cas qui ne devraient pas être many-to-many, ou des relations many-to-many utilisées comme raccourci.
  • Supprimer un enregistrement fait peur parce que vous ne savez pas ce que cela va casser ailleurs.
  • Les gens ne savent pas qui « possède » une donnée, alors ils la stockent là où c'est pratique.

Un exemple simple : vous avez users, orders, invoices, payments et tickets. Dans une configuration propre, chacun a un rôle clair. Dans une configuration spaghetti, tickets a un order_id, payments a un ticket_id, users a un last_invoice_id, et invoices pointent aussi vers users pour le « plan actuel ». Maintenant un bug de facturation implique quatre tables et deux idées différentes de ce que signifie « actuel ».

L'objectif ici est un nettoyage pragmatique : reconnaître les dépendances circulaires, les tables surchargées et la propriété floue, puis refondre vers plus de clarté et de maintenabilité. Pas de théorie profonde sur les bases, pas de débats sur la normalisation. Vous voulez un schéma qu'un nouveau coéquipier peut lire, interroger et modifier sans crainte.

Faites une cartographie rapide du schéma actuel

Avant de refondre quoi que ce soit, obtenez une image simple de ce que vous avez. Le but n'est pas un diagramme ER parfait, mais une carte partagée qui aide votre équipe à parler des mêmes tables de la même façon.

Commencez par lister chaque table et écrire une phrase simple sur ce qu'elle représente. Si vous ne pouvez pas décrire une table sans utiliser le nom d'une autre, considérez cela comme un signal d'alerte.

Puis décrivez les relations en mots courants : « un user a plusieurs orders », « une order a plusieurs items », « un product peut être dans plusieurs orders ». Restez simple. Vous essayez de voir l'intention et la forme, pas chaque cas particulier.

Une passe de 30 minutes qui aide vraiment

Limitez le temps et capturez seulement ce dont vous avez besoin pour prendre des décisions :

  • Table purpose : une phrase, plus les 3 colonnes principales qui la rendent unique
  • Key relationships : ce à quoi elle pointe (foreign keys) et ce qui pointe vers elle
  • Hot spots : tables touchées par beaucoup de fonctionnalités, écrans, jobs ou services
  • Write vs read : où l'application insère/met à jour vs où elle ne fait que sélectionner
  • Naming clashes : tables ou colonnes qui se ressemblent mais signifient des choses différentes

Après cela, marquez clairement vos hot spots. Une table « chaude » n'est pas automatiquement mauvaise, mais c'est là que le spaghetti commence souvent : les correctifs rapides s'y posent, des colonnes supplémentaires s'accumulent et chaque nouvelle fonctionnalité en dépend.

Capturez aussi le flux de données. Beaucoup de schémas désordonnés viennent du fait qu'on ne sait pas quelle est la source de vérité. Pour chaque table, notez qui l'écrit (flow d'inscription, panneau admin, job en arrière-plan, script d'import) et qui la lit (dashboards, reporting, recherche). Si une table est écrite depuis cinq endroits, attendez-vous à des règles incohérentes et des bugs surprenants.

Enfin, créez un minuscule glossaire. Choisissez les 5–10 mots qui causent des disputes : « account », « user », « customer », « workspace », « org », «member». Écrivez une phrase pour chacun. Par exemple : « Account = entité facturable. User = personne qui se connecte. Customer = société payante de l'account. » Cela évite que les débats sur la refonte tournent à la confusion linguistique.

Si vous avez hérité d'un prototype généré par IA, cette carte est souvent l'endroit où vous remarquez pour la première fois des doublons comme users, app_users et customers qui essaient tous de dire la même chose.

Comment identifier les dépendances circulaires

Une dépendance circulaire survient quand des tables dépendent les unes des autres en boucle. La version la plus simple est A pointe vers B, et B pointe vers A. Dans les applis réelles, cela devient souvent A -> B -> C -> A, et personne ne peut expliquer quel enregistrement est le parent.

Un exemple courant : vous avez users et teams. Un user appartient à une team (users.team_id). Quelqu'un ajoute aussi « owner de l'équipe » comme clé étrangère vers users (teams.owner_user_id). Maintenant créer la première team et le premier owner devient délicat : lequel doit exister en premier ?

Les cycles apparaissent à plusieurs endroits :

  • Auto-références comme categories.parent_id, surtout quand d'autres tables pointent aussi vers categories et que vous autorisez une imbrication profonde.
  • Tables de jointure qui deviennent silencieusement des entités « réelles », puis commencent à pointer en retour vers les deux côtés plus des tables additionnelles (roles, permissions, invitations).
  • Tables de lookup ou de status partagées qui grandissent en hub. Si statuses commence à référencer orders pour le « statut actuel de la commande », vous avez construit une boucle.

Vous pouvez détecter les cycles de deux façons : en lisant les foreign keys et en observant le comportement de l'app.

Depuis les foreign keys, dessinez une flèche pour chaque FK (child -> parent). Si vous pouvez suivre les flèches et revenir au point de départ, vous avez un cycle.

Dans le comportement de l'app, les cycles ressemblent souvent à :

  • Vous ne pouvez pas créer d'enregistrements sans astuces (FK nullables « juste pour l'instant », puis jamais corrigées).
  • Supprimer un enregistrement entraîne des suppressions bloquées, ou pire, des cascades inattendues.
  • Les mises à jour exigent des transactions en plusieurs étapes parce que chaque table a besoin de l'autre pour être valide.
  • Vous voyez des « lignes temporaires » ou des IDs de placeholder utilisés pendant l'inscription, le checkout ou l'onboarding.

Les dépendances circulaires sont pénibles car elles masquent la propriété. Il devient difficile de raisonner sur ce qui doit exister en premier, ce qui peut être supprimé sans risque et ce qui est vraiment optionnel.

Comment repérer les tables surchargées

Une table surchargée tente de représenter plusieurs choses du monde réel. Elle devient un tiroir à bazar : de nouveaux champs sont ajoutés parce que « c'est un peu lié », jusqu'à ce que la table contienne plusieurs significations à la fois.

Un test rapide : si vous ne pouvez pas décrire ce que représente une seule ligne en une phrase claire, la table est probablement surchargée.

Indices rapides dans le schéma

Vous pouvez souvent repérer le problème juste en parcourant les colonnes. Les tables surchargées ont beaucoup de champs nullables, des grappes claires de colonnes sans lien entre elles (facturation à côté de livraison à côté du support) et des motifs répétés comme *_status, *_date, *_note qui ressemblent à des workflows distincts entassés dans une même ligne. Un autre signe est une table avec des foreign keys vers des parties sans rapport de l'app (payments, marketing, support, inventaire) depuis un même endroit.

Aucun de ces signes pris isolément ne prouve un problème, mais quand plusieurs apparaissent ensemble, c'est un fort signal.

Indices cachés dans les données

Les données racontent souvent l'histoire plus clairement que le schéma.

Si vous interrogez la table et voyez des types d'enregistrements mélangés, vous remarquerez des valeurs et règles incohérentes. Par exemple, certaines lignes utilisent status = 'paid', d'autres status = 'closed', et d'autres la laissent vide parce que ce statut ne s'applique pas à ce type de ligne.

Un autre signe est une table où des « champs de type » sont partout : record_type, source_type, owner_type, target_type. Cela signifie souvent que la table joue le rôle de plusieurs tables déguisées.

Les tables surchargées sont des usines à bugs parce que différentes parties de l'app font des hypothèses différentes sur ce qu'est une ligne. Le reporting devient aussi peu fiable : deux équipes peuvent exécuter « total des enregistrements actifs » et obtenir des chiffres différents car chacune filtre un sous-ensemble différent de colonnes.

Comment décider quoi séparer

Lors de la refonte, les séparations tombent généralement dans trois catégories :

  • Split by concept : lorsque la table contient des noms différents (par exemple mélanger « customer », « vendor » et « employee »).
  • Split by lifecycle : lorsqu'une ligne couvre des étapes qui devraient être séparées (brouillon vs soumis vs réalisé, chacune avec des champs requis différents).
  • Split by actor : lorsque différentes équipes ou systèmes possèdent différentes parties du record (détails de paiement gérés par finance vs détails de ticket gérés par support).

Un test pratique : listez les 5 requêtes et écritures principales qui touchent la table. Si elles se répartissent naturellement en groupes séparés avec des règles et champs requis différents, vous avez trouvé un point de séparation propre.

Trouver la propriété floue et les concepts dupliqués

Obtenez rapidement un second avis
Notre équipe combine des outils IA et une vérification humaine avec un objectif de 99% de réussite.

Beaucoup de douleurs dans la refactorisation viennent d'un problème simple : personne ne sait quelle table est la source de vérité.

La propriété signifie qu'une table est l'endroit où un fait est créé et mis à jour, et que toutes les autres tables la traitent comme une référence en lecture seule (ou comme un cache clairement étiqueté). Quand la propriété est claire, les bugs se réparent plus facilement car vous savez où faire les changements. Quand elle est floue, de petites modifications deviennent des surprises parce que la même « vérité » existe à plusieurs endroits.

Où la propriété se complique le plus

La propriété se casse souvent après des prototypes rapides, surtout quand des gens copient des patterns depuis d'autres fonctionnalités.

Cherchez ces schémas :

  • Deux tables qui ressemblent toutes les deux à « le client » (par ex. customers et client_accounts) et qui sont toutes deux mises à jour par l'app.
  • Une table « profile » partagée utilisée par users, admins et vendors, où différents chemins de code s'écrasent mutuellement.
  • Statuts ou paramètres stockés à plusieurs endroits (une colonne dans users, plus une ligne user_settings, plus du JSON dans metadata).
  • Une table qui stocke faits business et champs de commodité UI ensemble (détails de facturation mélangés au display name et avatar).
  • Foreign keys qui pointent dans les deux sens parce qu'aucun côté ne « possède » la relation.

Détecter les concepts dupliqués avant de renommer

Les concepts dupliqués sont sournois car les noms diffèrent. Un moyen rapide de les trouver est de lister les noms métier clés (user, account, customer, org, order) puis rechercher dans votre schéma toutes les tables et colonnes qui les représentent.

Exemple : votre app a users.email et aussi contacts.email, et les deux sont modifiés lors de l'inscription. Maintenant il faut décider lequel pilote la connexion, les notifications et la facturation. Si l'app peut écrire dans les deux, vous aurez de la dérive.

La solution est de choisir une source de vérité et rendre les responsabilités explicites : une table peut écrire la valeur canonique ; les autres lisent ou mettent en cache, mais les caches doivent être clairement nommés et faciles à reconstruire.

Des règles de nommage simples réduisent rapidement l'ambiguïté :

  • Un mot pour un concept (customer vs client : choisissez-en un).
  • Mettez les champs canoniques dans la table propriétaire ; évitez de les dupliquer ailleurs.
  • Nommez les références de façon cohérente (customer_id, pas custId dans une table et client_id dans une autre).
  • Si vous devez mettre en cache, indiquez-le (customer_email_cached).

Principes de refonte pour garder les relations lisibles

Si vous voulez démêler les relations spaghetti, rendez les choses importantes évidentes : quel est le sujet du système, qui possède quoi et ce qui peut être supprimé ou modifié en toute sécurité plus tard.

Commencez par les quelques entités dont tout dépend

Choisissez le petit ensemble d'entités core qui doivent exister avant que quoi que ce soit d'autre fonctionne. Ce sont généralement des choses comme User, Account, Organization, Product, Order ou Invoice.

Si votre schéma fait dépendre une entité core d'enregistrements optionnels (logs, settings, tags), vous obtenez des insertions fragiles et des suppressions confuses.

Un test rapide : si une table ne peut pas être créée sans joindre trois autres tables, ce n'est probablement pas une entité core. C'est peut-être une table de relation ou une table de détails.

Gardez les relations explicites et prévisibles

Les bons schémas donnent une impression d'ennui — dans le bon sens. Quelques habitudes font une grande différence.

Séparez les données de référence (petites listes peu changeantes comme countries, statuses, plan types) des données transactionnelles (orders, payments, events). Les tables de référence devraient rarement dépendre des tables transactionnelles.

Utilisez des tables de jointure claires pour les many-to-many. Si users peuvent appartenir à plusieurs teams, préférez une table UserTeam avec juste des clés et quelques champs (role, created_at) plutôt que d'entasser des tableaux ou des colonnes dupliquées des deux côtés.

Soyez cohérent avec les primary keys et foreign keys. Choisissez un style de clé (UUID ou integer) et utilisez-le partout, sauf raison forte. Mélanger rend les jointures et le débogage plus difficiles.

Nommez les colonnes comme elles agissent. Utilisez team_id quand cela pointe vers Teams. Évitez des noms génériques comme ref_id ou data_id qui masquent la propriété.

Documentez le lifecycle en mots simples : ce qui est créé en premier, ce qui peut être créé plus tard et ce qui ne doit jamais être supprimé tant que d'autres enregistrements existent.

Voici un scénario concret : si Order a besoin d'un User, et que User a besoin de latest_order_id, vous avez une boucle qui provoque des inscriptions cassées et des écritures partielles. La solution consiste souvent à supprimer les clés étrangères « latest_* » du parent et à les calculer par requête (ou utiliser une table de synthèse séparée qui n'empêche pas les insertions).

Étapes : refactorer sans casser l'app

Rendez-le prêt pour la production
Préparez votre prototype IA pour la production avec des corrections vérifiées et une préparation au déploiement.

La façon la plus sûre de démêler les relations spaghetti est de traiter cela comme une chirurgie : zone limitée, plan clair et vérifications après chaque changement. Choisissez un workflow que vous pouvez décrire en une phrase, comme « créer une order » ou « inviter un coéquipier », au lieu d'essayer de réparer tout le schéma d'un coup.

Un schéma de migration sûr

Commencez par concevoir les nouvelles tables à côté des anciennes. Gardez les tables actuelles fonctionnelles pendant que vous ajoutez des tables plus propres avec des noms, clés et propriétés clairs. Par exemple, si une table mélange champs de profil, tokens d'auth et statut de facturation, scindez le design pour que chaque concept ait sa place.

Puis déplacez les données et le comportement par étapes :

  • Choisissez une petite zone cible et notez les requêtes exactes utilisées aujourd'hui.
  • Créez les nouvelles tables à côté des anciennes (ne supprimez ni ne renommez rien pour l'instant).
  • Backfillez les données des anciennes vers les nouvelles, puis validez les comptes et règles clés (clés uniques, foreign keys, colonnes not-null) avec des requêtes simples.

Après le backfill, migrez les lectures avant les écritures. Basculer d'abord les lectures permet de vérifier que l'app affiche toujours les mêmes résultats tandis que l'ancien chemin d'écriture continue d'alimenter les données. Une approche simple est d'ajouter un feature flag ou un toggle de configuration pour activer/désactiver les nouvelles lectures pendant les tests.

Une fois les lectures stables, changez les écritures avec précaution :

  • Basculez les écritures vers les nouvelles tables, et gardez une période courte où vous écrivez aussi dans les anciennes si le risque de rollback est élevé.
  • Supprimez les chemins morts et les anciennes colonnes seulement quand vous êtes sûr que rien n'en dépend.

Verrouillez-le pour qu'il reste propre

Ne vous arrêtez pas à la structure. Ajoutez des contraintes qui correspondent aux règles que vous voulez réellement (par ex. « une order doit avoir exactement un customer » ou « une membership doit être unique par user et workspace").

Ajoutez aussi de petites vérifications : un script lors de la migration qui compare les comptes de lignes, une requête quotidienne qui cherche des lignes orphelines, ou un test basique autour du workflow que vous venez de toucher.

Exemple : nettoyer un schéma users/orders en désordre

Un cas courant : le checkout fonctionne en dev, mais en production vous voyez des erreurs « user not found », des charges en double et des orders montrant la mauvaise adresse. Le schéma a souvent des tables familières (users, orders, payments), mais les relations sont assez emmêlées pour que de petits changements cassent quelque chose d'autre.

Voici un désordre typique :

  • Une seule table comme user_orders mélange infos utilisateur, champs de facturation, adresse de livraison, totaux de commande et statut de paiement.
  • users.last_order_id pointe vers orders.id, tandis que orders.user_id pointe vers users.id.
  • orders.payment_id pointe vers payments.id, mais payments.order_id pointe aussi vers orders.id.

Cette configuration crée deux problèmes à la fois.

D'abord, vous avez des dépendances circulaires : vous ne pouvez pas insérer une order sans payment, et vous ne pouvez pas insérer un payment sans order, donc l'app utilise des lignes temporaires ou des séquences d'update étranges.

Ensuite, la table est surchargée : chaque mise à jour d'un email ou d'une adresse utilisateur risque de réécrire d'anciennes orders (ou de laisser des orders incohérentes).

Une refonte plus propre est généralement une séparation avec une propriété claire :

  • users possède l'identité (email de connexion, nom, auth IDs).
  • addresses est possédé par users (plusieurs adresses par user).
  • orders est possédé par users (un user a plusieurs orders).
  • order_items est possédé par orders (une order a plusieurs items).
  • payments est possédé par orders (une order peut avoir une ou plusieurs tentatives de paiement).

Maintenant le flux d'insertion est simple : créer l'order, ajouter les items, puis créer une tentative de paiement. Pas d'IDs placeholder. Pas de colonne last_order_id nécessaire car le « dernier ordre » est une requête.

En termes de code, les requêtes deviennent plus claires : le checkout arrête de faire des mises à jour inter-tables pour tout synchroniser, et l'historique des commandes devient une jointure évidente users -> orders -> items.

Erreurs courantes qui aggravent le problème

Identifiez vos points chauds de schéma
Trouvez les quelques tables qui causent la majorité des pannes avant de tout réécrire.

La façon la plus rapide de retomber dans le piège est de « nettoyer » le schéma sans plan pour déplacer l'app et les données. La plupart des échecs ne sont pas des problèmes SQL. Ils viennent du fait que de petits changements se répercutent dans des jobs, des rapports et des cas limites que personne n'avait en tête.

Changements qui semblent propres mais créent des cassures

Une erreur classique est de scinder une table parce qu'elle paraît trop grosse, puis de mettre les nouvelles tables en production sans plan de migration. Si les anciennes et nouvelles tables sont écrites en parallèle pendant un moment, vous obtenez une dérive des données : deux sources de vérité qui ne se synchronisent jamais. L'app semble correcte jusqu'à ce qu'un remboursement, un ticket support ou un rapport de fin de mois expose la discordance.

Un autre problème courant est de supprimer des colonnes trop tôt. Les jobs en arrière-plan, exports, dashboards et écrans admin dépendent souvent de champs legacy bien après que le code principal ait cessé de les utiliser. Les supprimer sans inventaire complet transforme un nettoyage de schéma en incident production.

D'autres erreurs qui empirent la situation :

  • Ajouter encore plus de champs de « type » (user_type, order_type, entity_type) au lieu de modéliser de vraies relations avec des tables claires et des foreign keys.
  • Ignorer les contraintes et faire confiance uniquement au code applicatif, ce qui laisse passer de mauvaises données lors d'importations, scripts, retries ou futures fonctionnalités.
  • Renommer des concepts (« customer » en « account ») sans s'accorder sur les définitions, de sorte que différentes équipes utilisent le même mot pour dire des choses différentes.

Un exemple rapide de ce qui tourne mal

Imaginez une table users surchargée qui stocke aussi champs de facturation, appartenance org et infos lead. Quelqu'un la sépare en users, customers et leads, mais ne backfill pas de façon cohérente et ne verrouille pas qui possède l'email. Maintenant deux tables acceptent des mises à jour du même email et les outils support lisent le mauvais endroit. Le schéma semble plus propre sur le papier, mais la propriété est moins claire.

Une mentalité plus sûre : traitez les changements de schéma comme des changements produit. Rendre la propriété explicite, ajouter des contraintes tôt, migrer les données par étapes et garder les anciens champs tant que vous n'avez pas la preuve qu'ils ne sont plus nécessaires.

Checklist rapide et étapes pratiques suivantes

Quand un schéma semble emmêlé, vous n'avez pas besoin d'une grosse réécriture pour progresser. Commencez par quelques vérifications courtes qui montrent d'où vient la confusion, puis choisissez une petite refonte que vous pouvez terminer en toute sécurité.

Vérifications rapides (trouvez le désordre)

Cherchez les schémas qui créent vite des relations spaghetti :

  • Liens circulaires : table A dépend de B, B dépend de C et C dépend de A (souvent via des tables auxiliaires).
  • Concepts dupliqués : la même chose du monde réel stockée à plusieurs endroits (par ex. customer_id et buyer_id signifiant la même personne).
  • Tables surchargées : une table qui fait plein de choses (orders + payments + shipping + notes support entassés).
  • Source de vérité confuse : deux tables revendiquent la propriété (par ex. users et accounts stockent email et status).
  • Contraintes faibles : foreign keys manquantes, contraintes d'unicité manquantes et colonnes « n'importe quoi » qui cachent les erreurs.

Après en avoir repéré une ou deux, choisissez une zone unique (paiements ou identité utilisateur) et traitez-la comme un mini-projet.

Vérifications de sécurité (ne perdez pas de données)

Avant de changer la structure, planifiez comment prouver que rien n'a été cassé :

  • Faites une sauvegarde et confirmez que vous pouvez la restaurer.
  • Écrivez un plan de rollback pour chaque changement (même si c'est juste « gardez les anciennes colonnes jusqu'à vérification »).
  • Backfillez en étapes : créez d'abord les nouveaux champs ou tables, copiez les données, basculez les lectures, puis les écritures.
  • Vérifiez après backfill : comptes de lignes, totaux et contrôles manuels sur des enregistrements réels (y compris cas limites).
  • Ajoutez un monitoring basique : surveillez les taux d'erreur et les requêtes échouées pendant la fenêtre de déploiement.

La maintenabilité vient de décisions petites et claires. Donnez à chaque table un propriétaire unique (la maison de ce concept), choisissez des noms cohérents et appliquez des règles via des contraintes pour que les problèmes échouent tôt au lieu de se propager silencieusement.

Si vous gérez une app générée par IA et que le schéma vous résiste, traitez le code applicatif et le schéma comme un seul système. Refactorez un workflow bout en bout (schéma + requêtes + tests), puis passez au suivant.

Si vous voulez un second avis rapide avant de réécrire de larges parties, FixMyMess at fixmymess.ai fait des audits de code gratuits pour les bases de code générées par IA et aide à remédier à des problèmes comme des schémas emmêlés, une logique cassée et des failles de sécurité, généralement sous 48–72 heures.