Une différence silencieuse entre MySQL et PostgreSQL qui coûte cher en production
Quand on migre une application Laravel de MySQL vers PostgreSQL, on s'attend à retrouver les mêmes comportements fondamentaux. C'est généralement le cas, sauf sur un point précis que MySQL règle en coulisse et que PostgreSQL laisse volontairement de côté : l'indexation automatique des clés étrangères.
Sous MySQL, ajouter une contrainte de clé étrangère crée implicitement un index sur la colonne concernée. PostgreSQL ne fait rien de tel. Il pose la contrainte, valide la cohérence référentielle, et s'arrête là. L'index, si vous en avez besoin, c'est à vous de le créer. Et Laravel, via ->constrained(), ne le crée pas non plus.
Ce comportement est documenté, mais il passe souvent inaperçu lors d'une migration ou d'un développement classique, car les performances restent acceptables sur de petits volumes. Le problème se révèle plus tard, quand les tables grossissent.
Ce qui se passe concrètement lors d'une suppression ou d'un nettoyage
PostgreSQL applique l'intégrité référentielle à chaque modification : supprimer une ligne dans une table parent déclenche une vérification dans toutes les tables enfants référençant cette ligne. Si aucun index n'existe sur la colonne de jointure, PostgreSQL parcourt l'intégralité de la table enfant pour trouver les lignes concernées.
Sur une table orders contenant des millions de lignes et une colonne customer_id sans index, supprimer un client déclenche un scan séquentiel complet. Répété pour chaque parent supprimé, sur toute une plage de données, ce scan peut transformer un traitement de quelques secondes en plusieurs minutes d'attente.
Ce cas de figure apparaît fréquemment dans les jobs de nettoyage périodique (soft delete, archivage, expiration de sessions) où l'on supprime de nombreuses lignes parentes en boucle. C'est exactement là que l'absence d'index se manifeste le plus brutalement.
Le package Vacuum : détection statique et blocage en intégration continue
Vacuum est un package Laravel développé par Rati Rukhadze. Son principe repose sur la lecture des vues statistiques que PostgreSQL maintient en permanence : pg_stat_user_tables, pg_stat_user_indexes, pg_stat_activity, pg_stat_statements (si activée) et pg_class. Il n'instrumente pas vos requêtes et ne modifie pas votre base : il consulte ce que PostgreSQL sait déjà de son propre fonctionnement.
Parmi ses fonctionnalités, Vacuum identifie les clés étrangères sans index associé. Cette détection peut être intégrée dans un pipeline CI/CD pour bloquer automatiquement toute migration qui introduirait une clé étrangère non indexée. Concrètement, si un développeur pousse une migration avec ->foreign('customer_id')->constrained() sans ->index('customer_id'), le build échoue.
Pour l'intégrer dans un projet Laravel, l'installation est classique :
composer require --dev votre-vendor/vacuum
Ensuite, la commande d'analyse s'ajoute à l'étape de vérification du pipeline, aux côtés de PHPStan ou des tests unitaires. L'article original sur dev.to détaille le paramétrage et les limites du package, notamment le fait que Vacuum s'appuie sur les statistiques PostgreSQL, ce qui implique que la base de données de CI doit être dans un état représentatif pour que la détection soit fiable.
Corriger le problème : la pratique à adopter dans vos migrations
La correction est simple une fois qu'on a identifié les colonnes concernées. Dans une migration Laravel, il suffit d'ajouter explicitement l'index :
$table->foreignId('customer_id')->constrained()->index();
Ou de façon séparée :
$table->foreign('customer_id')->references('id')->on('customers');
$table->index('customer_id');
Pour les projets existants, un audit de la base permet de repérer les colonnes de clé étrangère sans index. La requête suivante sur PostgreSQL identifie ces cas :
SELECT
tc.table_name,
kcu.column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
LEFT JOIN pg_indexes AS pi
ON pi.tablename = tc.table_name
AND pi.indexdef LIKE '%' || kcu.column_name || '%'
WHERE tc.constraint_type = 'FOREIGN KEY'
AND pi.indexname IS NULL;
Ce résultat vous donne une liste de migrations correctives à prioriser selon le volume de données et la fréquence des opérations de suppression ou de mise à jour.
Conclusion
L'absence d'indexation automatique des clés étrangères sous PostgreSQL est un comportement intentionnel, pas un bug. MySQL fait un choix différent par défaut, ce qui crée une fausse habitude pour ceux qui migrent d'un moteur à l'autre. Laravel ne corrige pas cet écart, et aucun avertissement n'est levé lors de l'exécution des migrations.
Intégrer une vérification automatique dans le pipeline CI, via un outil comme Vacuum, permet de rendre ce problème impossible à ignorer. Couplé à une revue des migrations existantes sur les projets en production, cela garantit que les performances de votre application ne se dégradent pas discrètement à mesure que vos volumes de données augmentent.
Et votre site, il vaut quoi ?
Vitesse, accessibilité, référencement technique. Le rapport est écrit pour un dirigeant, pas pour un développeur. Gratuit, sans inscription.