← Retour aux articles
Performance et bases de données

Ce n'est pas votre base de données qui est lente, c'est votre requête : les bases de l'optimisation MySQL dans Laravel

Un guide pratique de l'optimisation MySQL dans Laravel : corriger les requêtes N+1, ajouter les bons index, éviter les boucles et transformer une page de 3 secondes en 150 ms sans changer de matériel.

✦
Article en vedette ↗

Ce n'est pas votre base de données qui est lente, c'est votre requête : les bases de l'optimisation MySQL dans Laravel que tout développeur devrait connaître

Chaque développeur Laravel est passé par là. Une page qui se chargeait en 200 ms prend soudainement 3 secondes. Votre premier réflexe est de blâmer la base de données : « MySQL est lent », « il nous faut un serveur plus puissant », « passons à PostgreSQL ». Mais voici la vérité inconfortable — votre base de données n'est presque jamais le problème. Ce sont vos requêtes.

MySQL est l'un des logiciels les plus éprouvés qui existent. Il peut gérer des millions de lignes et des milliers de connexions simultanées sans sourciller. Ce qu'il ne peut pas faire, c'est corriger comme par magie des requêtes mal écrites, des index manquants et le tristement célèbre problème N+1.

Dans cet article, nous allons passer en revue les techniques essentielles d'optimisation MySQL que tout développeur Laravel devrait connaître — celles qui transforment une page de 3 secondes en une page de 150 ms sans mettre à niveau le moindre composant matériel.

1. Le problème N+1 : le tueur silencieux de Laravel

C'est la cause la plus courante des applications Laravel lentes, et elle est presque toujours invisible jusqu'à ce que vous la cherchiez délibérément.

Considérez ce code d'apparence innocente :

<?php $posts = Post::all(); foreach ($posts as $post) { echo $post->author->name; }

Cela semble correct, non ? Sauf que cela génère N+1 requêtes. Une requête pour récupérer tous les articles, puis une requête supplémentaire pour chaque article afin de récupérer son auteur. Avec 100 articles, cela fait 101 requêtes. Avec 1 000 articles, cela fait 1 001 requêtes.

La solution est le chargement anticipé (eager loading) :

<?php $posts = Post::with('author')->get(); foreach ($posts as $post) { echo $post->author->name; }

Maintenant, ce ne sont plus que 2 requêtes, quel que soit le nombre d'articles.

Comment détecter les requêtes N+1

Laravel dispose d'excellents outils pour cela :

  • Laravel Debugbar — affiche chaque requête et met en évidence les doublons.
  • Laravel Telescope — un compagnon de débogage complet avec profilage des requêtes.
  • DB::listen() — journalisez chaque requête manuellement.
<?php DB::listen(function ($query) { Log::info($query->sql, $query->bindings); });

Vous pouvez également activer le mode strict en développement pour lever des exceptions lors du chargement paresseux :

<?php // Dans AppServiceProvider::boot() Model::preventLazyLoading(!app()->isProduction());

Cela vous force à détecter les problèmes N+1 pendant le développement plutôt qu'en production.

2. Les index : le plus grand gain unique

Si vous ne faites qu'une seule chose après avoir lu cet article, faites ceci : ajoutez des index à vos clés étrangères et à vos colonnes fréquemment interrogées.

Laravel rend cela facile avec les migrations :

<?php Schema::table('posts', function (Blueprint $table) { $table->index('user_id'); $table->index('status'); $table->index(['status', 'created_at']); // index composite });

Une requête qui parcourt 1 000 000 de lignes peut passer à 100 lignes avec le bon index. C'est une accélération de 10 000 fois grâce à une seule ligne de code.

Comment savoir quels index vous avez besoin

Utilisez EXPLAIN pour voir ce que fait MySQL :

<?php $posts = Post::where('status', 'published') ->where('user_id', 42) ->orderBy('created_at', 'desc') ->get(); // Voir le plan d'exécution DB::select('EXPLAIN ' . $posts->toSql(), $posts->getBindings());

Recherchez ces signaux d'alarme :

  • type: ALL — balayage complet de la table, aucun index utilisé.
  • rows: 500000 — MySQL s'attend à parcourir un demi-million de lignes.
  • Extra: Using filesort — tri en mémoire ou sur disque au lieu d'utiliser un index.
  • Extra: Using temporary — création d'une table temporaire, généralement pour GROUP BY.

La solution ? Ajoutez un index qui correspond au modèle de requête — généralement les colonnes dans WHERE, puis les colonnes dans ORDER BY.

3. Ne sélectionnez que ce dont vous avez besoin

Une autre erreur classique :

<?php $users = User::all(); // récupère chaque colonne pour chaque utilisateur

Si vous n'avez besoin que des noms et des e-mails, dites-le :

<?php $users = User::select('id', 'name', 'email')->get();

Cela compte plus que vous ne le pensez. Récupérer de grandes colonnes TEXT ou JSON que vous n'utilisez pas peut multiplier à la fois l'utilisation de la mémoire et le temps de requête. Et dans Laravel, chaque modèle Eloquent a un coût — construire des centaines de modèles complets quand vous n'avez besoin que de quelques champs est un gaspillage.

Pour des données véritablement en lecture seule, envisagez de contourner complètement Eloquent :

<?php $stats = DB::table('orders') ->select('status', DB::raw('COUNT(*) as total')) ->groupBy('status') ->get();

Query Builder est nettement plus rapide qu'Eloquent lorsque vous n'avez pas besoin de modèles.

4. Utilisez chunk() et cursor() pour les grands ensembles de données

Charger un million de lignes en mémoire fera planter votre application — même si la requête elle-même est rapide. Laravel fournit deux outils pour le streaming de grands ensembles de données :

<?php // chunk() — charge par lots User::chunk(1000, function ($users) { foreach ($users as $user) { // traiter } }); // cursor() — utilise un générateur, une ligne à la fois foreach (User::cursor() as $user) { // traiter }

cursor() utilise les générateurs PHP et est économe en mémoire, mais maintient une seule connexion à la base de données ouverte pendant toute l'itération. chunk() exécute plusieurs requêtes, ce qui est souvent plus sûr en production.

5. Évitez les requêtes dans les boucles

Si vous vous surprenez à écrire ceci :

<?php foreach ($userIds as $id) { $user = User::find($id); // ... }

Arrêtez. Remplacez-le par une seule requête :

<?php $users = User::whereIn('id', $userIds)->get();

Cela s'applique aussi aux mises à jour et aux suppressions :

<?php // Mauvais foreach ($userIds as $id) { User::where('id', $id)->update(['active' => false]); } // Bon User::whereIn('id', $userIds)->update(['active' => false]);

La seconde version est une requête au lieu de centaines — souvent la différence entre 500 ms et 5 ms.

6. Attention avec whereHas et withCount

whereHas() est pratique mais peut générer des sous-requêtes corrélées coûteuses :

<?php $users = User::whereHas('orders', function ($q) { $q->where('total', '>', 1000); })->get();

Pour de simples vérifications d'existence, préférez whereIn avec une sous-requête :

<?php $userIds = Order::where('total', '>', 1000) ->distinct() ->pluck('user_id'); $users = User::whereIn('id', $userIds)->get();

De même, withCount() exécute une sous-requête par relation. C'est acceptable pour quelques relations, mais si vous comptez cinq relations dans la même requête, envisagez une restructuration.

7. Cachez agressivement

Parfois, la requête la plus rapide est celle que vous n'exécutez pas du tout. La couche de cache de Laravel est l'un des outils de performance les plus sous-utilisés du framework.

<?php $topPosts = Cache::remember('top_posts', 3600, function () { return Post::orderByDesc('views')->limit(10)->get(); });

Pour les données qui changent rarement — catégories, paramètres, listes statiques — la mise en cache est essentiellement une performance gratuite. Utilisez Redis ou Memcached en production et n'oubliez pas d'invalider les clés lorsque les données changent.

8. Analysez les requêtes lentes en production

MySQL dispose d'un outil intégré pour trouver les requêtes lentes :

-- Activer le journal des requêtes lentes SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

Après une journée en production, examinez le journal. Vous trouverez les vrais coupables — les requêtes dont vous n'auriez jamais pensé qu'elles étaient lentes, exécutées des milliers de fois par minute.

Dans Laravel, vous pouvez également journaliser les requêtes lentes par requête via un service provider :

<?php // Dans AppServiceProvider::boot() if (app()->isProduction()) { DB::listen(function ($query) { if ($query->time > 1000) { // ms Log::warning('Slow query', [ 'sql' => $query->sql, 'time' => $query->time, ]); } }); }

9. Utilisez les bons types de données

Les petits détails comptent plus que vous ne le pensez :

  • Utilisez INT UNSIGNED pour les ID, pas VARCHAR.
  • Utilisez DECIMAL pour l'argent, jamais FLOAT.
  • Utilisez ENUM ou TINYINT pour les statuts, pas VARCHAR(255).
  • Utilisez CHAR(36) pour les UUID, pas VARCHAR(255).
  • Préférez DATETIME ou TIMESTAMP aux dates sous forme de chaînes.

Des colonnes plus petites signifient des index plus petits, ce qui signifie des recherches plus rapides. Cela s'accumule.

10. Optimisez les relations et les contraintes

Les migrations de Laravel facilitent la définition des clés étrangères :

<?php Schema::table('posts', function (Blueprint $table) { $table->foreignId('user_id')->constrained()->cascadeOnDelete(); });

Les clés étrangères ne préservent pas seulement l'intégrité des données — elles créent aussi automatiquement un index, ce qui est souvent exactement ce dont vous avez besoin pour la performance des JOIN.

Un workflow de débogage pratique

Lorsqu'une page est lente, suivez cette séquence :

  1. Installez Laravel Debugbar ou Telescope. Voyez combien de requêtes sont exécutées.
  2. Cherchez les modèles N+1. Ajoutez with() partout où une relation est accédée dans une boucle.
  3. Vérifiez le nombre de requêtes. Une page devrait généralement exécuter moins de 20 requêtes. Plus de 100 est un signal d'alarme.
  4. Exécutez EXPLAIN sur les requêtes les plus lentes. Recherchez les balayages complets de tables.
  5. Ajoutez des index. Faites correspondre l'index aux colonnes utilisées dans WHERE et ORDER BY.
  6. Cachez ce qui ne change pas. Surtout les agrégats et les données de configuration.
  7. Mesurez à nouveau. Répétez jusqu'à ce que la page soit rapide.

Conclusion

Neuf fois sur dix, « la base de données est lente » signifie en réalité « les requêtes sont lentes ». Avant de vous tourner vers un serveur plus puissant, un autre moteur de base de données ou un service cloud managé, passez un après-midi à optimiser vos requêtes. Vous découvrirez presque toujours que les gains sont déjà là — ils attendent dans le code.

Le chargement anticipé, les bons index, l'évitement des boucles et la mise en cache résoudront la majorité des problèmes de performance dans les applications Laravel. Aucune de ces techniques ne nécessite de nouvelle infrastructure. Elles nécessitent seulement de la conscience.

Commencez par votre page la plus lente. Lancez un journal de requêtes. Corrigez le N+1. Ajoutez un index manquant. Mesurez à nouveau. Puis répétez — et regardez votre base de données « devenir magiquement plus rapide » sans rien y changer.

Technologies et sujets

Tags de l'article

Aucun article ne correspond à ces filtres.

Vous avez un projet ou une idée à discuter ?

Parlons-en ↗