---
term: 'Problema delle query N+1'
seoTitle: 'Problema delle query N+1: cause, rilevamento e rimedi'
headline: 'Che cos''è il problema delle query N+1?'
slug: problema-n-plus-1
category: api-realtime
shortDefinition: 'Il problema delle query N+1 è un pattern in cui recuperare N record innesca una query in più per record – N+1 round trip invece di uno o due.'
relatedTerms:
  - relational-queries-document-databases
  - graphql-vs-rest
  - overfetching-underfetching
  - api-payload-optimization
  - database-index
contrastsWith:
  - overfetching-underfetching
faq:
  - question: 'Che cos''è il problema delle query N+1 in parole semplici?'
    answer: 'Si recupera un elenco di N record con una query, poi il codice emette una query in più per ogni record per i suoi dati collegati – cento post diventano centouno query. Ogni query è rapida di per sé, ed è esattamente per questo che il problema si nasconde: niente è abbastanza lento da allarmare qualcuno, finché la pagina non arriva a fare centinaia di round trip.'
  - question: 'Che cosa causa le query N+1?'
    answer: 'Il lazy loading come comportamento predefinito. ORM e SDK permettono di navigare le relazioni come proprietà di un oggetto – post.author – ed eseguono in modo trasparente una query al momento dell''accesso. Basta mettere quell''accesso dentro un ciclo su N risultati e si sono scritte N query in silenzio. L''astrazione che ha reso piacevole l''accesso ai dati ha reso invisibili anche i round trip.'
  - question: 'Come si risolve il problema N+1?'
    answer: 'Quattro vie d''uscita standard, da scegliere caso per caso: l''eager loading – dichiarare in anticipo sulla query di includere la relazione; un join che recupera entrambi in una sola istruzione; il batching – raccogliere le N chiavi esterne e recuperarle con una sola query di appartenenza a un insieme; e i loader con portata di richiesta, che raggruppano in automatico. Tutte e quattro trasformano N+1 round trip in uno o due.'
  - question: 'Quanto è più lento N+1 davvero?'
    answer: 'Basta fare il conto: ogni round trip costa da uno a cinque millisecondi prima che la query venga nemmeno eseguita, quindi 100 righe a 5 ms aggiungono mezzo secondo contro una sola query raggruppata da circa 10 ms. Le correzioni pubblicate riportano pagine passate da circa 1,4 secondi a 0,16 ed endpoint di API trenta volte più rapidi. Il conto peggiora in modo lineare con la dimensione della pagina – e in modo catastrofico attraverso la rete.'
  - question: 'Perché il problema N+1 è così comune in GraphQL?'
    answer: 'Perché i resolver girano per campo e per oggetto. Una query per i post con i loro autori esegue una volta il resolver dei post e N volte quello dell''autore – il ciclo dell''ORM rinato lato server. La cura canonica è il pattern loader: un batcher con portata di richiesta che raccoglie gli ID degli autori durante l''esecuzione ed emette un solo recupero raggruppato, memoizzato per la richiesta.'
  - question: 'Come si rilevano le query N+1?'
    answer: 'Conviene cercare molte query rapide e identiche, non una query lenta – quella è la firma. Il logging di debug dell''ORM mostra la stessa istruzione con parametri diversi; i log delle query lente la mancano del tutto, perché ogni singola query è veloce; gli strumenti di APM segnalano il pattern di span in modo esplicito. L''abitudine che la intercetta presto: leggere il log delle query di un solo rendering di pagina e contare.'
  - question: 'Il problema N+1 si presenta anche nei database documentali e nelle API REST?'
    answer: 'Ovunque i dati abbiano relazioni. Negli archivi documentali è una find per ogni documento referenziato – si risolve con i meccanismi include, con query di appartenenza a un insieme raggruppate oppure con l''incorporamento. Su REST è un endpoint di elenco più una chiamata HTTP per elemento – peggio della versione sul database, perché la latenza di rete fa impallidire quella delle query; endpoint composti, endpoint di batch e linguaggi di query esistono in gran parte per eliminarlo.'
  - question: 'Il lazy loading è sempre sbagliato?'
    answer: 'No – è sbagliato dentro i cicli. Il lazy loading è esattamente giusto quando i dati collegati servono raramente: si paga sull''unico record che ne ha bisogno, invece di pagare in anticipo per tutti gli N. La disciplina sta nel sapere quale pattern di accesso ha ciascuna schermata: le relazioni sempre necessarie si caricano in modo anticipato, quelle raramente necessarie in modo lazy, e tutto ciò che sta dentro un ciclo passa sotto 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: 'Una sola richiesta dove altri ne fanno cento'
  text: 'Gli SDK di Back4app fanno del rimedio l''idioma predefinito: include() recupera le relazioni nella stessa richiesta, raggruppate lato server, e i pointer tengono i join a basso costo. Il ciclo N+1 non ha semplicemente motivo di esistere nella tua codebase.'
  linkText: 'Inizia gratis'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-10-09'
translationKey: n-plus-one-query-problem
---

**Il problema delle query N+1 è un pattern in cui recuperare N record innesca una query in più per record – N+1 round trip invece di uno o due.** È il bug di prestazioni grave più comune nelle applicazioni basate sui dati, ed è anche il più mimetizzato: ogni singola query è rapida, il codice si legge alla perfezione e la pagina funziona – fino al momento in cui arrivano i volumi di dati reali.

## Punti chiave

| Domanda | Risposta |
| --- | --- |
| La forma | 1 query per l'elenco + N query per le relazioni = N+1 round trip |
| La causa | Lazy loading toccato dentro un ciclo – query invisibili a ogni iterazione |
| La firma | Molte query *rapide e identiche* – i log delle query lente non le vedono mai |
| Le vie d'uscita | Eager loading · join · IN per lotti · loader con portata di richiesta |
| Il moltiplicatore | Latenza di round trip × dimensione della pagina – brutale in rete |

## Il bug, a carte scoperte

```javascript
// 1 query: recupera 100 post
const posts = await postRepo.findRecent(100);

for (const post of posts) {
  // +1 query PER OGNI POST – post.author sembra una proprietà,
  // ma scatta il lazy loading: SELECT * FROM users WHERE id = ?
  render(post.title, (await post.author).name);
}
// Log delle query: 1 + 100 = 101 round trip per un solo rendering di pagina
```

Il rimedio, come lo esprimono gli SDK – dichiarare la relazione in anticipo, e la piattaforma la recupera nella stessa richiesta:

**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"))
  }
}
```

## Il costo, conti alla mano

L'aritmetica che le spiegazioni saltano – tempo totale ≈ query di elenco + (N × latenza di round trip):

| Dimensione pagina N | @1 ms/query | @5 ms/query | Per lotti (1–2 query) |
| --- | --- | --- | --- |
| 10 | ~11 ms | ~55 ms | ~10 ms |
| 100 | ~101 ms | ~505 ms | ~12 ms |
| 1.000 | ~1,0 s | ~5,0 s | ~20 ms |

Le correzioni pubblicate confermano il conto: pagine che scendono da 1,4 s a 0,16 s, endpoint 30× più rapidi e oltre. Due corollari da fissare: **la paginazione mette un tetto a N** – una dimensione di pagina limitata circoscrive il raggio d'impatto ancora prima del rimedio vero; e **la versione in rete è peggiore** – quando ognuna delle N chiamate è una richiesta HTTP invece di una query locale, si moltiplica per decine di millisecondi anziché per unità.

## Le quattro vie d'uscita

```mermaid
flowchart LR
  accTitle: Cascata N+1 contro recupero per lotti
  accDescr: Il pattern N+1 emette una query di elenco seguita da una query per ogni record, una dopo l'altra; il pattern per lotti emette la query di elenco e poi una sola query che recupera in un colpo tutti i record collegati.
  subgraph P["Cascata N+1"]
    a["Query di elenco"] --> b["Query per record<br/>× N, una dopo l'altra"]
  end
  subgraph B["Per lotti"]
    c["Query di elenco"] --> d["UNA query per tutte le relazioni<br/>WHERE id IN (…)"]
  end
  P -.->|"il rimedio"| B
```

| Rimedio | Come | Da preferire quando | Attenzione a |
| --- | --- | --- | --- |
| Eager loading / include | Dichiarare le relazioni sulla query | La schermata ha sempre bisogno della relazione | Includere troppo gonfia i payload |
| Join | Una sola istruzione recupera entrambi | Motore relazionale, letture di tipo report | L'esplosione di righe sui join larghi |
| IN per lotti | Raccogliere le chiavi, recuperare una volta | Qualunque stack, anche fatto a mano | Un round trip in più (accettabile) |
| Loader con portata di richiesta | Raggruppamento automatico durante l'esecuzione | Resolver GraphQL, codice a livelli | Deve essere per richiesta, non globale |

L'ultima riga è il caso celebre di GraphQL: i resolver scattano per ogni oggetto genitore e ricreano il ciclo lato server, e il [pattern loader](https://www.graphql-js.org/docs/n1-dataloader/) – raccogliere le chiavi in un tick, recuperare una volta, memoizzare per richiesta – è il [rimedio standard del settore](https://shopify.engineering/solving-the-n-1-problem-for-graphql-through-batching). Una regola sottile viaggia con esso: i loader hanno *portata di richiesta*; un loader globale diventa una cache obsoleta piena di bug di autorizzazione.

## Rilevamento: dare la caccia alle query rapide

La firma di N+1 è l'inverso del normale lavoro sulle prestazioni: si cercano **molte query rapide e identiche**, non una query lenta – ed è per questo che i log delle query lente, lo strumento abituale, [non la vedono mai](https://docs.sentry.io/product/issues/issue-details/performance-issues/n-one-queries/). I metodi che funzionano: il logging di debug dell'ORM (la stessa istruzione, N parametri diversi, un solo rendering); le viste a span dell'APM, dove la cascata di span brevi e identici è inconfondibile; e il meno costoso di tutti – contare le query di un singolo caricamento di pagina in sviluppo, con una soglia in testa: una pagina di elenco deve costare un numero di query a una cifra, non un multiplo delle sue righe. Anche il gemello lato scrittura merita il suo audit: un ciclo con un insert per elemento è N+1 in scrittura, e si risolve con operazioni in massa (bulk).

## Casi d'uso tipici

- **Viste a elenco con autori, proprietari o stati** – la casa canonica: ogni feed, inbox e tabella che collega persone a elementi.
- **API GraphQL** – i campi di elenco annidati sono strutturalmente N+1 finché non esistono i loader.
- **Database documentali** – una find per ogni documento referenziato; si risolve con include, IN per lotti o incorporamento, secondo le [regole di modellazione](/glossary/it/join-nei-database-documentali/).
- **Fan-out tra microservizi** – un elenco dal servizio A, una chiamata HTTP al servizio B per ogni elemento; gli endpoint composti e i BFF esistono per porre fine a questo.
- **Job in background** – il ciclo che elabora 10.000 record con due query ciascuno, costando ore in silenzio.

## Lazy vs. eager loading: matrice decisionale

| Caricare in anticipo quando… | Restare lazy quando… |
| --- | --- |
| La relazione compare su ogni riga | La relazione sta dietro un clic |
| Il ciclo è il pattern di accesso | L'accesso avviene un record per volta |
| N ha la dimensione di una pagina o più | N è garantito minuscolo |
| La latenza è visibile all'utente | Un task in background può permettersi uno scostamento |
| Questo bug è stato appena corretto qui | C'è una misurazione, non un'ipotesi |

E la regola di audit permanente che sopravvive a qualunque matrice: **qualunque relazione toccata dentro un ciclo è colpevole finché il log delle query non dimostra il contrario.**

## Limiti e trade-off

- **L'eager loading può correggere troppo.** Includere relazioni pesanti ovunque scambia N+1 con payload gonfiati e join larghi – conviene includere ciò che la schermata mostra, non l'intero grafo.
- **Anche i join hanno il loro salto nel vuoto.** I join uno-a-molti duplicano le righe del genitore per ogni figlio; a fan-out alto il recupero per lotti in due query batte il singolo join.
- **I loader aggiungono macchinario.** La portata di richiesta, l'invalidazione della cache dentro la richiesta e le finestre di raggruppamento sono codice vero – è il prezzo del batching automatico.
- **I framework lo reintroducono in silenzio.** Serializzatori, helper dei template e revisioni del tipo "solo un campo in più" sono il modo in cui le pagine corrette regrediscono; il controllo del numero di query va nella CI, non nella memoria delle persone.
- **Il rimedio è per percorso, non globale.** N+1 è un bug di pattern di accesso; ogni nuova schermata ripropone la domanda.

## N+1 su Back4app

Back4app è una piattaforma open-source di Backend as a Service (BaaS) che combina database gestito, API REST e GraphQL generate automaticamente, autenticazione, archiviazione di file e funzioni serverless con Cloud Code. I suoi SDK fanno del rimedio l'idioma corrente invece della correzione a posteriori: le relazioni sono Pointer tipizzati, e [`include()`](https://docs.parseplatform.org/js/guide/#relational-data) – i tab di codice qui sopra – li recupera nella stessa richiesta, raggruppati lato server. L'API GraphQL risolve le query annidate senza fan-out per campo, e i valori predefiniti di paginazione limitano N prima che metta i denti. Il ciclo che causa N+1 non ha un modo naturale di essere scritto – che è il miglior tipo di rimedio: quello che nessuno deve ricordare.
