Django: eliminare le query N+1 con select_related e prefetch_related
2026-10-14
Il problema delle query N+1 è la causa più comune di pagine Django lente: una query carica N oggetti, poi ne parte una in più per ciascuno per leggere una relazione. Con 10 articoli sono 11 query; con 1000, 1001.
Un modello di esempio
class Autore(models.Model):
nome = models.CharField(max_length=50)
class Tag(models.Model):
nome = models.CharField(max_length=30)
class Articolo(models.Model):
titolo = models.CharField(max_length=100)
autore = models.ForeignKey(Autore, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag)
Il codice che sembra innocuo
for a in Articolo.objects.all():
print(a.autore.nome) # una query per ogni articolo
Correzione 1: select_related per ForeignKey e OneToOne
Esegue una JOIN SQL e carica l'autore insieme all'articolo:
for a in Articolo.objects.select_related("autore"):
print(a.autore.nome) # nessuna query aggiuntiva
Correzione 2: prefetch_related per ManyToMany e relazioni inverse
Fa una seconda query sola per tutte le relazioni e le unisce in Python:
qs = Articolo.objects.select_related("autore").prefetch_related("tags")
for a in qs:
print([t.nome for t in a.tags.all()])
Attenzione: a.tags.filter(...) o a.tags.order_by(...) ignorano il prefetch e rilanciano la query. Per filtrare usa Prefetch("tags", queryset=Tag.objects.filter(...)).
Numeri misurati
Con 10 articoli, 10 autori e 3 tag (SQLite in memoria, Django 4.2, conteggio con CaptureQueriesContext):
| Caso | Query |
|---|---|
Ciclo naive su autore |
11 |
select_related("autore") |
1 |
tags.all() senza prefetch |
11 |
select_related + prefetch_related("tags") |
2 |
Lo script completo e riproducibile è scaricabile dal repository del sito (verifica/n_plus_uno.py).
Come accorgersene
- In sviluppo usa
django-debug-toolbar: il pannello SQL segnala query duplicate. - Nei test usa
assertNumQueries(2)per bloccare regressioni. - Con
CaptureQueriesContext(connection)conti le query di un blocco di codice, come nell'esempio sopra.
Regola pratica: select_related per relazioni "verso uno", prefetch_related per relazioni "verso molti".