Quand votre schéma devient un boulet
Dans le développement d'applications web à forte croissance, les choix architecturaux pris en début de projet ont tendance à se retourner contre vous au pire moment : celui où votre base de données commence à peiner sous la charge. Un pattern en particulier illustre parfaitement ce phénomène : les relations polymorphiques.
L'idée semble séduisante au départ. Vous avez besoin d'attacher des métadonnées flexibles à plusieurs modèles — un utilisateur, une entreprise, une facture — et Laravel vous offre clé en main une table unique avec des colonnes preferenceable_id et preferenceable_type. C'est propre, c'est élégant, ça prend dix minutes à mettre en place.
Sauf qu'à l'échelle, c'est une bombe à retardement.
Le piège des jointures polymorphiques
Considérons un cas concret : vous souhaitez retrouver tous les utilisateurs ayant activé le mode sombre (dark_mode = true). Avec une table polymorphique centralisée, votre base de données doit scanner l'intégralité de la table — entreprises, factures, commandes incluses — pour isoler les lignes correspondant aux utilisateurs. Ce sont des LEFT JOIN massifs sur des données sans rapport entre elles.
Les conséquences sont bien réelles :
- Des requêtes lentes qui dégradent l'expérience utilisateur sous charge
- Un schéma illisible qui complique les évolutions et les onboardings
- Des coûts d'infrastructure qui explosent à mesure que vous tentez de compenser avec plus de mémoire ou de CPU
C'est ce qu'on appelle la dette technique silencieuse : elle ne fait pas de bruit tant que le trafic reste modéré, mais elle se manifeste brutalement le jour où votre application commence à vraiment cartonner.
La solution : colonnes JSONB et index GIN
PostgreSQL propose depuis plusieurs années un type de colonne particulièrement adapté à ce problème : JSONB (JSON Binary). L'idée est simple : stocker des données flexibles et non structurées directement dans la ligne de la table parente, sans table intermédiaire, sans jointure.
Mais ce qui rend JSONB vraiment puissant, c'est la possibilité de créer des index GIN (Generalized Inverted Index) sur ces colonnes. Cela signifie que PostgreSQL peut interroger les structures JSON imbriquées avec des performances comparables à celles d'une colonne indexée classique.
Mise en place concrète
Première étape : supprimer la table polymorphique et ajouter une colonne preferences de type JSONB directement sur le modèle concerné.
// Migration
Schema::table('users', function (Blueprint $table) {
$table->jsonb('preferences')->nullable();
});
// Ajout de l'index GIN pour les performances
DB::statement('CREATE INDEX idx_users_preferences ON users USING GIN (preferences)');
Du côté du modèle Eloquent, le cast natif de Laravel prend en charge la sérialisation et désérialisation automatique :
class User extends Model
{
protected $casts = [
'preferences' => 'array',
];
}
Requêtes performantes dans le JSON
PostgreSQL expose des opérateurs dédiés pour interroger les données JSONB. Dans Laravel, vous pouvez les utiliser via whereRaw :
// Tous les utilisateurs avec dark_mode activé
$users = User::whereRaw("preferences @> ?::jsonb", [json_encode(['dark_mode' => true])])->get();
// Lecture et écriture via Eloquent
$user->preferences = array_merge($user->preferences ?? [], ['dark_mode' => true]);
$user->save();
L'opérateur @> (contient) est nativement supporté par l'index GIN : la requête sera rapide quelle que soit la volumétrie, car PostgreSQL n'a pas besoin de scanner toutes les lignes.
Ce que vous gagnez concrètement
La migration vers JSONB n'est pas qu'une optimisation de performance. Elle apporte plusieurs bénéfices structurels :
Schéma simplifié : fini les tables satellites dont le rôle devient flou au fil des mois. Chaque modèle porte ses propres données flexibles, la lisibilité s'en trouve considérablement améliorée.
Évolutivité sans migration lourde : ajouter un nouveau champ de préférence ne nécessite pas de migration de schéma. Vous ajoutez simplement la clé dans le JSON — idéal pour des fonctionnalités en A/B test ou des configurations per-tenant dans une architecture SaaS.
Performances prévisibles : avec un index GIN en place, les temps de réponse restent stables à mesure que votre base de données grossit. Vous ne découvrez pas un problème de performance en production le jour J.
Coûts maîtrisés : moins de jointures = moins de charge CPU et mémoire sur votre serveur de base de données. Sur des infrastructures cloud facturées à la ressource consommée, l'impact est directement visible.
Symfony et l'écosystème PHP : même combat
Bien que l'article source de Prajapati Paresh sur dev.to soit centré sur Laravel, ces principes s'appliquent pleinement à Symfony avec Doctrine. Le type json de Doctrine mappe nativement sur les colonnes JSON/JSONB de PostgreSQL, et les index GIN peuvent être configurés via des annotations ou du SQL brut dans les migrations.
L'essentiel à retenir : ce n'est pas un problème de framework, c'est un problème de modélisation. La tentation de la relation polymorphique existe dans tout écosystème, et la réponse JSONB est universellement disponible dès lors que vous travaillez avec PostgreSQL.
Conclusion
Les relations polymorphiques ont leur place — pour des projets de petite taille ou des cas d'usage très spécifiques. Mais si vous construisez une application appelée à scaler, chaque LEFT JOIN sur une table partagée est une dette que vous payerez un jour, avec intérêts.
Migrer vers des colonnes JSONB indexées via GIN, c'est choisir de payer le coût de la bonne décision maintenant, plutôt que le coût de la mauvaise décision plus tard — souvent en prod, souvent sous pression.
C'est le genre d'arbitrage architectural qui distingue une application qui tient la charge d'une application qui rend les armes au mauvais moment.