---
term: 'Le Problème des Requêtes N+1'
seoTitle: 'Le problème des requêtes N+1 : causes, détection et correctifs'
headline: "Qu'est-ce que le problème des requêtes N+1 ?"
slug: probleme-n-plus-1
category: api-realtime
shortDefinition: "Le problème des requêtes N+1 est un pattern où lire N enregistrements déclenche une requête de plus pour chacun — N+1 allers-retours au lieu d'un ou deux."
relatedTerms:
  - relational-queries-document-databases
  - graphql-vs-rest
  - overfetching-underfetching
  - api-payload-optimization
  - database-index
contrastsWith:
  - overfetching-underfetching
faq:
  - question: "Qu'est-ce que le problème des requêtes N+1, en termes simples ?"
    answer: "Vous récupérez une liste de N enregistrements en une requête, puis votre code fait une requête de plus par enregistrement pour ses données liées — cent posts deviennent cent une requêtes. Chaque requête est rapide prise isolément, et c'est précisément pour ça que le problème se cache : rien n'est assez lent pour alerter qui que ce soit, jusqu'à ce que la page fasse des centaines d'allers-retours."
  - question: 'Quelles sont les causes des requêtes N+1 ?'
    answer: "Le chargement paresseux (lazy loading) comme comportement par défaut. Les ORM et les SDK vous laissent naviguer dans les relations comme dans des propriétés d'objet — post.author — et exécutent une requête de façon transparente dès que vous en touchez une. Placez cet accès dans une boucle sur N résultats et vous avez écrit N requêtes sans le savoir. L'abstraction qui a rendu l'accès aux données agréable a aussi rendu les allers-retours invisibles."
  - question: 'Comment corriger le problème N+1 ?'
    answer: "Quatre issues standard, à choisir au cas par cas : le chargement anticipé (eager loading) — indiquer d'emblée à la requête d'inclure la relation ; une jointure qui récupère les deux en une seule instruction ; le traitement par lot (batch) — collecter les N clés étrangères et les récupérer en une seule requête de type contained-in ; et des loaders à portée de requête qui regroupent automatiquement. Les quatre transforment N+1 allers-retours en un ou deux."
  - question: 'À quel point le N+1 est-il vraiment plus lent ?'
    answer: "Faites le calcul : chaque aller-retour coûte une à cinq millisecondes avant même que la requête s'exécute, donc 100 lignes à 5 ms ajoutent une demi-seconde, contre une seule requête par lot d'environ 10 ms. Des correctifs réels publiés font état de pages passant d'environ 1,4 seconde à 0,16, et d'endpoints d'API trente fois plus rapides. Le calcul se dégrade linéairement avec la taille de page — et de façon catastrophique à travers le réseau."
  - question: 'Pourquoi le problème N+1 est-il si courant en GraphQL ?'
    answer: "Parce que les resolvers s'exécutent par champ et par objet. Une requête sur des posts avec leurs auteurs exécute le resolver des posts une fois et celui de l'auteur N fois — la boucle de l'ORM ressuscitée côté serveur. Le remède canonique est le pattern loader : un regroupeur à portée de requête qui collecte les ID d'auteurs pendant l'exécution et émet une seule récupération par lot, mémoïsée pour la durée de la requête."
  - question: 'Comment détecter les requêtes N+1 ?'
    answer: "Cherchez beaucoup de requêtes rapides et identiques, pas une seule requête lente — c'est la signature. Les logs de débogage de l'ORM montrent l'instruction répétée avec des paramètres différents ; les journaux de requêtes lentes passent complètement à côté puisque chaque requête est rapide ; les outils d'APM signalent explicitement le motif de spans. L'habitude qui l'attrape tôt : lisez le journal des requêtes pour le rendu d'une page, et comptez."
  - question: 'Le problème N+1 existe-t-il dans les bases documentaires et les API REST ?'
    answer: "Partout où les données ont des relations. Dans les bases documentaires, c'est un find par document référencé — corrigé par des mécanismes include, des requêtes contained-in groupées ou l'embarquement. En REST, c'est un endpoint de liste plus un appel HTTP par élément — pire que la version base de données, car la latence réseau écrase la latence de requête ; les endpoints composés, les endpoints batch et les langages de requête existent en grande partie pour l'éliminer."
  - question: 'Le lazy loading est-il toujours une erreur ?'
    answer: "Non — c'est une erreur dans les boucles. Le lazy loading est exactement le bon choix quand les données liées sont rarement nécessaires : payez-les pour le seul enregistrement qui en a besoin plutôt que d'avance pour les N. La discipline consiste à connaître le pattern d'accès de chaque écran : les relations toujours nécessaires se chargent en eager, les relations rarement nécessaires en lazy, et tout ce qui se trouve dans une boucle passe à l'audit."
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'DataLoader pattern — graphql-js documentation'
    url: 'https://www.graphql-js.org/docs/n1-dataloader/'
  - name: 'Solving the N+1 problem for GraphQL through batching — Shopify Engineering'
    url: 'https://shopify.engineering/solving-the-n-1-problem-for-graphql-through-batching'
  - name: 'N+1 queries — Sentry performance issue documentation'
    url: 'https://docs.sentry.io/product/issues/issue-details/performance-issues/n-one-queries/'
  - name: 'SDK query documentation (include)'
    url: 'https://docs.parseplatform.org/js/guide/#relational-data'
cta:
  title: 'Une requête là où les autres en font cent'
  text: "Les SDK de Back4app font du correctif l'idiome par défaut : include() récupère les relations dans la même requête, regroupées côté serveur, avec des pointers qui gardent les jointures peu coûteuses. La boucle N+1 n'a tout simplement aucune raison d'exister dans votre codebase."
  linkText: 'Commencez gratuitement'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-09-11'
translationKey: n-plus-one-query-problem
---

**Le problème des requêtes N+1 est un pattern où lire N enregistrements déclenche une requête de plus pour chacun — N+1 allers-retours au lieu d'un ou deux.** C'est le bug de performance sérieux le plus répandu dans les applications adossées à des données, et le mieux camouflé : chaque requête individuelle est rapide, le code se lit parfaitement et la page fonctionne — jusqu'à ce que les vrais volumes de données arrivent.

## Points clés

| Question | Réponse |
| --- | --- |
| La forme | 1 requête pour la liste + N requêtes pour les relations = N+1 allers-retours |
| La cause | Du lazy loading sollicité dans une boucle — des requêtes invisibles à chaque itération |
| La signature | Beaucoup de requêtes *rapides et identiques* — les journaux de requêtes lentes ne la voient jamais |
| Les issues | Eager loading · jointures · IN par lot · loaders à portée de requête |
| Le multiplicateur | Latence d'aller-retour × taille de page — brutal à travers le réseau |

## Le bug, au grand jour

```javascript
// 1 requête : récupère 100 posts
const posts = await postRepo.findRecent(100);

for (const post of posts) {
  // +1 requête PAR POST — post.author ressemble à une propriété,
  // mais le lazy loading exécute : SELECT * FROM users WHERE id = ?
  render(post.title, (await post.author).name);
}
// Journal des requêtes : 1 + 100 = 101 allers-retours pour un seul rendu de page
```

Le correctif, tel que l'expriment les SDK — déclarez la relation d'emblée, et la plateforme la récupère dans la même requête :

**JavaScript:**

```javascript
// JavaScript / Node.js — Back4app JS SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
const query = new Parse.Query('Comment');
query.equalTo('post', post);
query.include('author');                    // eager-load the pointer
const comments = await query.find();        // one round trip, total

comments.forEach((c) =>
  render(c.get('text'), c.get('author').get('username')) // already loaded
);
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
final query = QueryBuilder<ParseObject>(ParseObject('Comment'))
  ..whereEqualTo('post', post.toPointer())
  ..includeObject(['author']);              // eager-load the pointer
final response = await query.query();       // one round trip, total
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
let query = Comment.query("post" == post)
  .include("author")                        // eager-load the pointer
query.find { result in
  if case .success(let comments) = result { // one round trip, total
    comments.forEach { render($0.text, $0.author?.username) }
  }
}
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
val query = ParseQuery.getQuery<ParseObject>("Comment")
query.whereEqualTo("post", post)
query.include("author")                     // eager-load the pointer
query.findInBackground { comments, e ->     // one round trip, total
  if (e == null) comments.forEach {
    render(it.getString("text"), it.getParseObject("author")?.getString("username"))
  }
}
```

## Le coût, calcul à l'appui

L'arithmétique que les explications sautent — temps total ≈ requête de liste + (N × latence d'aller-retour) :

| Taille de page N | @1 ms/requête | @5 ms/requête | Par lot (1–2 requêtes) |
| --- | --- | --- | --- |
| 10 | ~11 ms | ~55 ms | ~10 ms |
| 100 | ~101 ms | ~505 ms | ~12 ms |
| 1 000 | ~1,0 s | ~5,0 s | ~20 ms |

Les correctifs publiés confirment le calcul : des pages qui passent de 1,4 s à 0,16 s, des endpoints 30× plus rapides, voire davantage. Deux corollaires à retenir : **la pagination plafonne N** — une taille de page bornée limite le rayon d'impact avant même le vrai correctif ; et **la version réseau est pire** — quand chacun des N appels est une requête HTTP plutôt qu'une requête locale, multipliez par des dizaines de millisecondes au lieu de quelques unités.

## Les quatre issues

```mermaid
flowchart LR
  accTitle: Cascade N+1 contre récupération par lot
  accDescr: Le pattern N+1 émet une requête de liste suivie d'une requête par enregistrement, l'une après l'autre ; le pattern par lot émet la requête de liste puis une seule requête qui récupère d'un coup tous les enregistrements liés.
  subgraph P["Cascade N+1"]
    a["Requête de liste"] --> b["Requête par enregistrement<br/>× N, l'une après l'autre"]
  end
  subgraph B["Par lot"]
    c["Requête de liste"] --> d["UNE requête pour toutes les relations<br/>WHERE id IN (…)"]
  end
  P -.->|"le correctif"| B
```

| Correctif | Comment | À privilégier quand | Attention à |
| --- | --- | --- | --- |
| Eager loading / include | Déclarer les relations sur la requête | L'écran a toujours besoin de la relation | Trop inclure fait gonfler les payloads |
| Jointure | Une instruction récupère les deux | Moteur relationnel, lectures de type rapport | L'explosion de lignes sur les jointures larges |
| IN par lot | Collecter les clés, récupérer une fois | N'importe quel stack, même fait main | Un aller-retour de plus (acceptable) |
| Loader à portée de requête | Regroupement automatique pendant l'exécution | Resolvers GraphQL, code en couches | Doit être par requête, pas global |

La dernière ligne est le cas célèbre de GraphQL : les resolvers se déclenchent par objet parent et recréent la boucle côté serveur, et le [pattern loader](https://www.graphql-js.org/docs/n1-dataloader/) — collecter les clés en un tick, récupérer une fois, mémoïser par requête — est le [remède standard du secteur](https://shopify.engineering/solving-the-n-1-problem-for-graphql-through-batching). Une règle subtile l'accompagne : les loaders sont *à portée de requête* ; un loader global devient un cache périmé truffé de bugs d'autorisation.

## Détection : traquez les requêtes rapides

La signature du N+1 est l'inverse du travail de performance habituel : vous cherchez **beaucoup de requêtes rapides et identiques**, pas une requête lente — c'est pourquoi les journaux de requêtes lentes, l'outil habituel, [ne la voient jamais](https://docs.sentry.io/product/issues/issue-details/performance-issues/n-one-queries/). Les méthodes qui fonctionnent : les logs de débogage de l'ORM (la même instruction, N paramètres différents, un seul rendu) ; les vues de spans de l'APM, où la cascade de spans courts et identiques est impossible à manquer ; et la moins coûteuse de toutes — comptez les requêtes d'un chargement de page en développement, avec un seuil en tête : une page de liste devrait coûter un nombre de requêtes à un chiffre, pas un multiple de ses lignes. Le jumeau côté écriture mérite aussi son audit : une boucle d'insert par élément, c'est du N+1 en écriture, corrigé par des opérations en masse (bulk).

## Cas d'usage courants

- **Vues en liste avec auteurs, propriétaires ou statuts** — le terrain canonique : chaque fil, boîte de réception et tableau qui relie des personnes à des éléments.
- **API GraphQL** — les champs de liste imbriqués sont structurellement N+1 tant qu'il n'y a pas de loaders.
- **Bases documentaires** — un find par document référencé ; corrigé par include, IN par lot ou embarquement, selon les [règles de modélisation](/glossary/fr/jointures-bases-documentaires/).
- **Fan-outs de microservices** — une liste venant du service A, un appel HTTP au service B par élément ; les endpoints composés et les BFF existent pour y mettre fin.
- **Jobs en arrière-plan** — la boucle qui traite 10 000 enregistrements avec deux requêtes chacun, et qui coûte des heures en silence.

## Lazy vs. eager loading : matrice de décision

| Chargez en eager quand… | Restez en lazy quand… |
| --- | --- |
| La relation s'affiche sur chaque ligne | La relation est derrière un clic |
| La boucle est le pattern d'accès | L'accès se fait un enregistrement à la fois |
| N a la taille d'une page ou plus | N est garanti minuscule |
| La latence est visible par l'utilisateur | Une tâche en arrière-plan peut se permettre d'attendre |
| Vous venez de corriger ce bug à cet endroit | Vous avez mesuré, pas supposé |

Et la règle d'audit permanente qui survit à toutes les matrices : **toute relation sollicitée dans une boucle est coupable jusqu'à ce que le journal des requêtes prouve le contraire.**

## Limites et trade-offs

- **L'eager loading peut surcorriger.** Inclure partout des relations lourdes échange le N+1 contre des payloads gonflés et des jointures larges — incluez ce que l'écran affiche, pas le graphe entier.
- **Les jointures ont leur propre falaise.** Les jointures un-à-plusieurs dupliquent les lignes du parent pour chaque enfant ; avec un fan-out élevé, la récupération par lot en deux requêtes bat la jointure unique.
- **Les loaders ajoutent de la mécanique.** Portée par requête, invalidation du cache au sein de la requête et fenêtres de regroupement sont du vrai code — le prix du regroupement automatique.
- **Les frameworks le réintroduisent en silence.** Serializers, helpers de templates et revues "juste un champ de plus" sont la façon dont les pages corrigées régressent ; le contrôle du nombre de requêtes a sa place dans la CI, pas dans la mémoire.
- **Le correctif est par chemin, pas global.** Le N+1 est un bug de pattern d'accès ; chaque nouvel écran repose la question.

## Le N+1 sur Back4app

Back4app est une plateforme open-source de Backend as a Service (BaaS) qui combine une base de données gérée, des API REST et GraphQL générées automatiquement, l'authentification, le stockage de fichiers et des fonctions serverless avec Cloud Code. Ses SDK font du correctif l'idiome plutôt que la réparation : les relations sont des Pointers typés, et [`include()`](https://docs.parseplatform.org/js/guide/#relational-data) — les onglets de code ci-dessus — les récupère dans la même requête, regroupées côté serveur. L'API GraphQL résout les requêtes imbriquées sans fan-out par champ, et les valeurs de pagination par défaut bornent N avant qu'il ne montre les dents. La boucle qui cause le N+1 n'a aucune façon naturelle d'être écrite — et c'est le meilleur type de correctif : celui dont personne n'a à se souvenir.
