Che cos'è il problema delle query N+1?

Aggiornato: ottobre 2026

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

DomandaRisposta
La forma1 query per l’elenco + N query per le relazioni = N+1 round trip
La causaLazy loading toccato dentro un ciclo – query invisibili a ogni iterazione
La firmaMolte query rapide e identiche – i log delle query lente non le vedono mai
Le vie d’uscitaEager loading · join · IN per lotti · loader con portata di richiesta
Il moltiplicatoreLatenza di round trip × dimensione della pagina – brutale in rete

Il bug, a carte scoperte

// 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 / 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
);

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/queryPer 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

Cascata N+1 contro recupero per lottiIl 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.

il rimedio

Per lotti

Query di elenco

UNA query per tutte le relazioni
WHERE id IN (…)

Cascata N+1

Query di elenco

Query per record
× N, una dopo l'altra

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.
RimedioComeDa preferire quandoAttenzione a
Eager loading / includeDichiarare le relazioni sulla queryLa schermata ha sempre bisogno della relazioneIncludere troppo gonfia i payload
JoinUna sola istruzione recupera entrambiMotore relazionale, letture di tipo reportL’esplosione di righe sui join larghi
IN per lottiRaccogliere le chiavi, recuperare una voltaQualunque stack, anche fatto a manoUn round trip in più (accettabile)
Loader con portata di richiestaRaggruppamento automatico durante l’esecuzioneResolver GraphQL, codice a livelliDeve 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 – raccogliere le chiavi in un tick, recuperare una volta, memoizzare per richiesta – è il rimedio standard del settore. 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. 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.
  • 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 rigaLa relazione sta dietro un clic
Il ciclo è il pattern di accessoL’accesso avviene un record per volta
N ha la dimensione di una pagina o piùN è garantito minuscolo
La latenza è visibile all’utenteUn task in background può permettersi uno scostamento
Questo bug è stato appena corretto quiC’è 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() – 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.

Domande frequenti

Che cos'è il problema delle query N+1 in parole semplici?

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.

Che cosa causa le query N+1?

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.

Come si risolve il problema N+1?

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.

Quanto è più lento N+1 davvero?

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.

Perché il problema N+1 è così comune in GraphQL?

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.

Come si rilevano le query N+1?

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.

Il problema N+1 si presenta anche nei database documentali e nelle API REST?

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.

Il lazy loading è sempre sbagliato?

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.

Termini correlati

Confronta con

Fonti di approfondimento

Pronto a costruire il tuo backend?

Avvia il tuo progetto su Back4app in pochi minuti: database, autenticazione, API e Cloud Code inclusi. Nessuna carta di credito richiesta.

Scritto e revisionato da Back4app Engineering, Back4app Engineering · Pubblicato il 2026-10-09