Le problème de requête N+1 dans Laravel est un souci de performance sournois que presque tout projet basé sur Eloquent rencontre tôt ou tard. Le code paraît propre, les tests passent, la page se charge vite en local — puis quelques milliers de lignes s'accumulent en base et cette même page se met soudain à ramer. La cause est souvent une seule chose : envoyer des dizaines, voire des centaines de requêtes supplémentaires à l'intérieur d'une boucle sans s'en rendre compte. Dans cet article, je vous explique pourquoi cela arrive, comment repérer les requêtes cachées grâce au query log, et comment régler le problème définitivement avec l'eager loading.
Qu'est-ce que le problème N+1, exactement ?
Le nom dit tout. Une requête parente (1) s'exécute et renvoie N enregistrements ; ensuite, pour chacun de ces N enregistrements, une autre requête (N) s'exécute pour récupérer les données liées. Soit 1 + N requêtes au total. Si vous listez 20 articles et affichez l'auteur de chacun, 1 requête part pour les articles et 20 pour les auteurs : 21 requêtes.
Prenons l'exemple classique. Sur une liste de blog, on veut afficher le nom de l'auteur de chaque article :
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name;
}
La première ligne produit une seule requête select * from posts. Mais l'expression $post->author dans la boucle déclenche une requête select * from users where id = ? distincte pour chaque article. Eloquent charge la relation en lazy loading (chargement paresseux), c'est-à-dire au moment du premier accès. Donc 100 articles font 101 requêtes, et vous ne le remarqueriez jamais en regardant simplement le résultat affiché.
Repérer les requêtes cachées avec le query log
La première étape pour régler le problème, c'est de le voir. La façade DB de Laravel peut enregistrer chaque requête exécutée. Vous pouvez l'activer temporairement dans une route ou un contrôleur ainsi :
use Illuminate\Support\Facades\DB;
DB::enableQueryLog();
$posts = Post::all();
foreach ($posts as $post) {
$post->author->name;
}
dd(DB::getQueryLog());
Si vous voyez 101 entrées dans la sortie, le coupable est démasqué. La même requête SQL répétée encore et encore avec where id = ? est la signature la plus nette du N+1.
Une approche plus durable consiste à journaliser chaque requête dans un service provider :
DB::listen(function ($query) {
logger()->info($query->sql, $query->bindings);
});
Placez ceci dans la méthode boot de AppServiceProvider et surveillez storage/logs/laravel.log pour voir précisément combien de requêtes une seule requête HTTP exécute. En développement, des outils comme Laravel Debugbar ou Telescope affichent aussi ce compteur automatiquement en bas de chaque page ; au lieu de parcourir les requêtes répétées à l'œil, vous remarquez simplement le compteur grimper.
La solution : l'eager loading avec with()
La solution est étonnamment simple. Au lieu de charger la relation ligne par ligne dans la boucle, vous demandez d'emblée à Eloquent de « récupérer aussi cette relation » pendant que la requête parente s'exécute. Cela se fait avec with() :
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name;
}
Maintenant Eloquent n'exécute que deux requêtes : une pour les articles, et une qui récupère tous les auteurs d'un coup avec select * from users where id in (1, 2, 3, ...). Deux requêtes au lieu de 101 pour 100 articles. C'est ainsi que le N+1 devient 1 + 1.
Vous pouvez aussi charger plusieurs relations, et des relations imbriquées, en même temps :
$posts = Post::with(['author', 'comments.user', 'tags'])->get();
Ici, la syntaxe pointée comments.user charge aussi l'utilisateur de chaque commentaire ; ainsi, parcourir les commentaires ne crée pas un nouveau N+1. Si vous voulez alléger la requête en ne sélectionnant que certaines colonnes :
$posts = Post::with('author:id,name')->get();
Attention : dans le sous-ensemble de colonnes, vous devez toujours inclure la clé étrangère de la relation — id ici — sinon Eloquent ne peut pas associer les enregistrements et la relation revient à null.
Autres techniques utiles
- loadMissing() : si vous avez déjà les modèles et n'êtes pas sûr que la relation soit chargée, cette méthode comble les manques sans lancer de requêtes redondantes :
$posts->loadMissing('author'). - withCount() : si vous n'affichez que le nombre d'enregistrements liés, obtenez le compte en une seule requête au lieu de récupérer toutes les lignes :
Post::withCount('comments')->get(). Le résultat arrive sous$post->comments_count. - Protection automatique : Laravel vous permet d'interdire totalement le lazy loading. Ajoutez
Model::preventLazyLoading(! app()->isProduction())dansAppServiceProvideret accéder à une relation non eager-loadée lève une exception en développement. Vous attrapez ainsi le N+1 pendant que vous écrivez encore le code.
L'eager loading est puissant, mais ne prenez pas le réflexe de coller with() partout. Si vous n'utilisez pas réellement une relation sur la page, la charger se retourne contre vous et gonfle la mémoire. La règle est simple : toute relation accédée dans une boucle doit être eager-loadée ; aucune relation jamais accédée ne doit l'être.
Questions fréquentes
Le problème N+1 est-il toujours un vrai problème ?
Sur de petits jeux de données, il passe inaperçu ; 6 requêtes pour 5 enregistrements ne gênent personne. Mais à mesure que les données grossissent, le nombre de requêtes croît de façon linéaire, et des centaines de requêtes étouffent à la fois la base et le temps de réponse. Le pire, c'est qu'il est invisible en local et explose en production — d'où l'intérêt de mesurer tôt.
Quelle est la différence entre with() et load() ?
with() planifie la relation pendant la construction de la requête, avant que les modèles soient récupérés. load() ajoute une relation après coup à une collection que vous avez déjà. Les deux utilisent la même logique de requête groupée ; le choix dépend du moment où vous avez obtenu les modèles.
Comment rendre l'eager loading obligatoire en test ?
Activez Model::preventLazyLoading() uniquement hors production. Ainsi, en test et en développement, toute relation que vous oubliez d'eager-loader lève une exception, tandis qu'en production le comportement reste normal et discret.
Votre application Laravel est plus lente que prévu ? Le coupable est souvent des requêtes N+1 cachées. Nous pouvons profiler votre projet ensemble et réduire le nombre de requêtes — contactez-moi.