Le problème des requêtes N+1 : le tueur silencieux de performance dans presque toutes les applications web
Votre page fait 3 requêtes en développement et 3 000 en production. C’est le problème N+1 — le bug de performance le plus fréquent que je trouve dans les applications Laravel, Prisma et Rails, comment le repérer en quelques minutes, et comment le corriger pour de bon.
Il existe un bug de performance si fréquent que je le cherche dans les dix premières minutes de chaque audit de base de données, et si silencieux que des applications le livrent pendant des années. C’est le problème des requêtes N+1, et si votre application utilise un ORM — Eloquent de Laravel, Prisma, Django, Rails, peu importe — il y a de bonnes chances que vous en ayez un quelque part en ce moment, attendant sagement que vos données grandissent.
Ce que N+1 signifie réellement
Vous chargez une liste de N éléments, puis, pour chaque élément, vous lancez une requête de plus pour récupérer un élément lié. 100 commandes sur une page, puis 100 requêtes séparées pour le client de chaque commande. Total : 101 requêtes là où 2 suffiraient. Affichez 1 000 lignes et ce sont 1 001 requêtes.
Le code qui cause cela a l’air parfaitement innocent. En Laravel :
$orders = Order::all();
foreach ($orders as $order) {
echo $order->customer->name; // une requête par commande, cachée dans une propriété
}En Prisma, c’est la boucle qui attend une relation par élément au lieu d’utiliser include. L’ORM rend les requêtes supplémentaires invisibles — aucune ligne de code ne dit « requête » — ce qui est précisément pourquoi le bug survit aux revues de code. Et il évolue dans la pire direction possible : plus votre page a de succès, plus elle lance de requêtes.
Pourquoi cela fait plus mal que le décompte brut ne le suggère : chaque requête est un aller-retour réseau plus une analyse plus une planification. Une requête de 0,5 ms, ce n’est rien ; 2 000 d’affilée, c’est une seconde entière de surcharge pure avant que la base fasse un quelconque travail réel — et sous des utilisateurs simultanés, cette foule de petites requêtes brûle aussi les connexions et le CPU, ralentissant les requêtes de tout le monde, pas seulement les vôtres. Je le retrouve constamment derrière la plainte « le site marche pour moi mais s’effondre aux heures de pointe ».
Comment le repérer (en minutes, honnêtement)
Comptez les requêtes par page. Laravel Telescope ou Debugbar, le journal de requêtes de Prisma, Django Debug Toolbar — n’importe lequel vous montrera le nombre. Une page de liste qui lance des centaines de requêtes a un N+1 jusqu’à preuve du contraire.
Cherchez le motif dans le journal. Des dizaines de requêtes quasi identiques ne différant que par un ID (
SELECT * FROM customers WHERE id = ?× 300), c’est l’empreinte digitale. Une requête pour la liste + des répétitions pour les détails = N+1.Vérifiez là où ça ne fait pas encore mal. Les données de développement sont petites, donc le N+1 se cache. Triez vos pages par « requêtes × lignes affichées » et vérifiez les pires cas contre des volumes de données réalistes, pas vos 12 enregistrements de test.
Les corrections, selon la situation
Le chargement anticipé (la correction à 90 %). Dites à l’ORM dès le départ ce dont vous aurez besoin : Order::with('customer') en Eloquent, include: { customer: true } en Prisma, select_related/prefetch_related en Django. L’ORM récupère alors les relations en une requête supplémentaire avec une liste IN (...), et vos 1 001 requêtes deviennent 2.
Une jointure, quand la relation est toujours nécessaire et filtrée. Si vous filtrez ou triez par les données liées (« les commandes dont le client est au Maroc »), le chargement anticipé ne suffit pas — c’est le travail d’une jointure SQL, ou d’un whereHas en termes Eloquent.
Ne sélectionnez que les colonnes utilisées. La moitié des « N+1 » que je trouve voyagent avec un SELECT * sur des tables larges. Récupérer trois colonnes au lieu de quarante réduit chacune de ces requêtes et la mémoire qu’elles consomment.
Regroupez et mettez en cache les chemins vraiment chauds. Un en-tête de tableau de bord qui interroge les mêmes données de référence pour chaque utilisateur n’a pas besoin d’ingéniosité — il a besoin d’un cache. (Savoir quand le cache est la bonne réponse plutôt qu’un pansement est un sujet en soi — voir mon guide du cache Redis.)
Agrégez en SQL, pas en PHP/JS. Compter, sommer et moyenner dans des boucles applicatives après avoir récupéré les lignes est le cousin du N+1. COUNT, SUM et GROUP BY existent pour que la base — qui est juste à côté des données — fasse l’arithmétique une seule fois.
L’habitude de prévention
Les corrections pourrissent sans habitude derrière. Trois qui ne coûtent rien :
Des budgets de requêtes en revue de code. « Cette page doit tourner en moins de 15 requêtes » est une règle qu’un relecteur peut réellement vérifier, barre d’outils de débogage ouverte.
Des données de test réalistes. Une base de développement avec 10 000 commandes au lieu de 10 rend les N+1 visibles tant qu’ils sont encore gratuits à corriger.
Surveillez le journal des requêtes lentes en production. Les requêtes quasi identiques répétées à l’échelle s’y annoncent en premier — la même discipline de surveillance que dans mon guide des bases lentes.
Le problème N+1 est, en un sens, une bonne nouvelle : c’est un bug à la forme connue, à la détection bon marché et, le plus souvent, à la correction en une ligne par occurrence — et en supprimer un mauvais peut ramener une page de trois secondes à 200 millisecondes sans toucher ni serveur ni index. Si vos pages les plus fréquentées semblent lourdes et que vous ignorez combien de requêtes elles lancent, c’est votre première mesure à prendre. Et si vous voulez un œil expérimenté sur le journal, c’est mon travail quotidien — les premiers constats sont généralement d’une simplicité embarrassante.