# Django ORM: ottimizzare le query per prestazioni massime > Guida completa all'ottimizzazione delle query con il Django ORM. select_related, prefetch_related, indici, analisi del problema N+1 e tecniche avanzate per applicazioni ad alte prestazioni. - Published: 2026-02-15 - Updated: 2026-04-28 - Author: SharpSkill - Tags: django, django orm, python, ottimizzazione database, prestazioni - Reading time: 14 min --- Il Django ORM offre un'astrazione elegante per interagire con il database, ma questa semplicità può nascondere problemi di prestazioni critici. Un'applicazione Django mal ottimizzata può generare centinaia di query là dove ne basterebbe una sola. Questa guida illustra le tecniche essenziali per individuare e risolvere tali problemi. > **La regola d'oro dell'ottimizzazione** > > Prima di ottimizzare, occorre misurare. L'utilizzo di django-debug-toolbar in fase di sviluppo permette di visualizzare ogni query SQL generata e di individuare rapidamente i colli di bottiglia. ## Comprendere il problema N+1 Il problema N+1 rappresenta la trappola più comune con gli ORM. Si verifica quando una query iniziale recupera N oggetti e poi vengono eseguite N query aggiuntive per accedere alle relazioni di ciascun oggetto. Questa moltiplicazione di query degrada gravemente le prestazioni. ```python # models.py from django.db import models class Author(models.Model): """Modello che rappresenta l'autore di un libro.""" name = models.CharField(max_length=200) email = models.EmailField(unique=True) bio = models.TextField(blank=True) def __str__(self): return self.name class Book(models.Model): """Modello che rappresenta un libro con il suo autore.""" title = models.CharField(max_length=300) author = models.ForeignKey( Author, on_delete=models.CASCADE, related_name='books' ) published_date = models.DateField() isbn = models.CharField(max_length=13, unique=True) def __str__(self): return self.title ``` Questi modelli semplici permettono di illustrare il problema N+1 in modo concreto. ```python # views.py - Esempio problematico def list_books_bad(request): """❌ Questa view genera N+1 query.""" books = Book.objects.all() # 1 query per i libri for book in books: # Ogni accesso a book.author scatena una query SQL print(f"{book.title} di {book.author.name}") # Con 100 libri = 101 query SQL! return render(request, 'books/list.html', {'books': books}) ``` Il codice sembra innocuo ma genera una query per libro per recuperare l'autore associato. ## Risolvere il problema N+1 con select_related Il metodo `select_related` esegue un JOIN SQL e recupera i dati correlati in un'unica query. Funziona con relazioni ForeignKey e OneToOneField. ```python # views.py - Soluzione ottimizzata con select_related def list_books_optimized(request): """✅ Questa view genera una sola query con JOIN.""" # select_related esegue un JOIN SQL books = Book.objects.select_related('author').all() for book in books: # Nessuna query aggiuntiva: l'autore è già caricato print(f"{book.title} di {book.author.name}") # Totale: 1 sola query SQL indipendentemente dal numero di libri return render(request, 'books/list.html', {'books': books}) ``` La query SQL generata utilizza un LEFT OUTER JOIN per recuperare gli autori insieme ai libri. ```python # Concatenazione di select_related per relazioni annidate # models.py class Publisher(models.Model): name = models.CharField(max_length=200) country = models.CharField(max_length=100) class Book(models.Model): title = models.CharField(max_length=300) author = models.ForeignKey(Author, on_delete=models.CASCADE) publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) # views.py def list_books_with_details(request): """Recupera libri, autori e editori in una sola query.""" books = Book.objects.select_related( 'author', # ForeignKey verso Author 'publisher' # ForeignKey verso Publisher ).all() return render(request, 'books/list.html', {'books': books}) ``` Più relazioni possono essere ottimizzate contemporaneamente elencandole in `select_related`. ## Ottimizzare le relazioni ManyToMany con prefetch_related Per le relazioni ManyToMany o le relazioni inverse (ForeignKey vista dall'altro lato), `prefetch_related` esegue query separate ma ottimizzate, evitando JOIN imponenti. ```python # models.py class Tag(models.Model): """Tag per categorizzare i libri.""" name = models.CharField(max_length=50, unique=True) def __str__(self): return self.name class Book(models.Model): title = models.CharField(max_length=300) author = models.ForeignKey(Author, on_delete=models.CASCADE) tags = models.ManyToManyField(Tag, related_name='books') ``` La relazione ManyToMany tra Book e Tag richiede `prefetch_related` per un'ottimizzazione efficace. ```python # views.py - Ottimizzazione ManyToMany def list_books_with_tags(request): """✅ Recupera i libri e i loro tag in modo efficiente.""" books = Book.objects.prefetch_related('tags').all() for book in books: # I tag sono precaricati, nessuna query aggiuntiva tag_names = [tag.name for tag in book.tags.all()] print(f"{book.title}: {', '.join(tag_names)}") # Totale: 2 query (libri + tag) indipendentemente dal volume return render(request, 'books/list.html', {'books': books}) ``` `prefetch_related` esegue una query separata per i tag e poi effettua il join in Python. > **select_related vs prefetch_related** > > Utilizzare `select_related` per ForeignKey e OneToOne (JOIN SQL). Utilizzare `prefetch_related` per ManyToMany e relazioni inverse (query separate). I due si possono combinare nella stessa QuerySet. ## Personalizzare il precaricamento con gli oggetti Prefetch L'oggetto `Prefetch` consente un controllo fine sui dati precaricati: filtraggio, ordinamento e perfino la limitazione del numero di risultati. ```python # views.py from django.db.models import Prefetch def list_authors_with_recent_books(request): """Recupera gli autori solo con i loro libri recenti.""" # Prefetch personalizzato: solo i libri dal 2025 in poi recent_books_prefetch = Prefetch( 'books', queryset=Book.objects.filter( published_date__year__gte=2025 ).order_by('-published_date'), to_attr='recent_books' # Memorizzato in un attributo personalizzato ) authors = Author.objects.prefetch_related( recent_books_prefetch ).all() for author in authors: # Accesso tramite l'attributo personalizzato for book in author.recent_books: print(f"{author.name}: {book.title}") return render(request, 'authors/list.html', {'authors': authors}) ``` L'attributo `to_attr` salva i risultati in una lista Python invece del manager abituale. ```python # Combinazione avanzata: select_related + Prefetch def list_authors_complete(request): """Esempio completo di ottimizzazione su più livelli.""" authors = Author.objects.prefetch_related( Prefetch( 'books', queryset=Book.objects.select_related( 'publisher' # Ottimizza anche l'editore di ogni libro ).prefetch_related( 'tags' # E i tag di ogni libro ).filter(published_date__year=2026) ) ).all() return render(request, 'authors/complete.html', {'authors': authors}) ``` Questo approccio riduce drasticamente il numero di query per strutture dati complesse. ## Limitare le colonne con only() e defer() Per impostazione predefinita, Django recupera tutte le colonne della tabella. Per modelli con molti campi o campi di grandi dimensioni, limitare le colonne migliora le prestazioni. ```python # views.py def list_books_minimal(request): """Recupera solo le colonne necessarie.""" # only() specifica le colonne da includere books = Book.objects.only( 'id', 'title', 'published_date' ).select_related('author') # Attenzione: accedere a un campo non incluso scatena una query for book in books: print(book.title) # OK, incluso # print(book.isbn) # Genererebbe una query aggiuntiva return render(request, 'books/list.html', {'books': books}) ``` Il metodo `only()` crea un oggetto «deferred» che carica solo le colonne specificate. ```python # defer() per escludere colonne specifiche def list_authors_without_bio(request): """Esclude campi grandi raramente utilizzati.""" # defer() esclude le colonne indicate authors = Author.objects.defer( 'bio' # Il TextField non viene caricato ).all() for author in authors: print(author.name) # OK print(author.email) # OK # author.bio caricherebbe il campo su richiesta return render(request, 'authors/list.html', {'authors': authors}) ``` `defer()` è l'inverso di `only()`: le colonne elencate non vengono caricate inizialmente. ## Ottimizzare con values() e values_list() Quando servono solo alcuni valori senza oggetti modello completi, `values()` e `values_list()` restituiscono dizionari o tuple più leggeri. ```python # views.py def get_book_titles(request): """Recupera solo i titoli come una lista.""" # values_list restituisce tuple titles = Book.objects.values_list('title', flat=True) # Risultato: ['Libro 1', 'Libro 2', ...] # values restituisce dizionari book_data = Book.objects.values('title', 'published_date') # Risultato: [{'title': 'Libro 1', 'published_date': ...}, ...] return render(request, 'books/titles.html', {'titles': titles}) ``` Questi metodi evitano l'istanziazione degli oggetti modello, riducendo il consumo di memoria. ```python # Combinazione con aggregazioni from django.db.models import Count, Avg def get_author_statistics(request): """Statistiche per autore senza caricare gli oggetti.""" stats = Author.objects.values('name').annotate( book_count=Count('books'), avg_year=Avg('books__published_date__year') ).order_by('-book_count') # Risultato: [{'name': 'Autore', 'book_count': 5, 'avg_year': 2024}, ...] return render(request, 'authors/stats.html', {'stats': stats}) ``` L'annotazione consente di eseguire i calcoli direttamente nel database. ## Creare indici per accelerare le query Gli indici del database accelerano drasticamente le ricerche. Django permette di definirli direttamente nei modelli. ```python # models.py from django.db import models class Book(models.Model): title = models.CharField(max_length=300, db_index=True) author = models.ForeignKey(Author, on_delete=models.CASCADE) published_date = models.DateField() isbn = models.CharField(max_length=13, unique=True) status = models.CharField(max_length=20, default='available') class Meta: # Indici composti per query frequenti indexes = [ # Indice sulla data di pubblicazione (ordinamento frequente) models.Index( fields=['published_date'], name='book_pub_date_idx' ), # Indice composto per filtrare per autore + stato models.Index( fields=['author', 'status'], name='book_author_status_idx' ), # Indice parziale: solo libri disponibili models.Index( fields=['published_date'], name='book_available_idx', condition=models.Q(status='available') ), ] # Ordinamento predefinito che sfrutta l'indice ordering = ['-published_date'] ``` Questi indici migliorano le prestazioni delle query che filtrano su tali colonne. > **Indici e operazioni di scrittura** > > Gli indici accelerano le letture ma rallentano leggermente le scritture (INSERT, UPDATE). Conviene crearli solo sulle colonne usate spesso nelle clausole WHERE, ORDER BY o JOIN. ## Ricorrere a query SQL grezze quando necessario Per query complesse o ottimizzazioni specifiche del database, le query SQL grezze offrono il pieno controllo. ```python # views.py from django.db import connection def get_books_with_raw_sql(request): """Query grezza per casi particolari.""" # Metodo 1: raw() per recuperare oggetti modello books = Book.objects.raw(''' SELECT b.*, a.name as author_name FROM library_book b INNER JOIN library_author a ON b.author_id = a.id WHERE b.published_date > %s ORDER BY b.published_date DESC ''', ['2025-01-01']) return render(request, 'books/list.html', {'books': books}) def execute_custom_query(request): """Esecuzione diretta per query non SELECT.""" with connection.cursor() as cursor: # Query con aggregazione complessa cursor.execute(''' SELECT a.name, COUNT(b.id) as book_count, AVG(EXTRACT(YEAR FROM b.published_date)) as avg_year FROM library_author a LEFT JOIN library_book b ON b.author_id = a.id GROUP BY a.id, a.name HAVING COUNT(b.id) > 2 ORDER BY book_count DESC ''') results = cursor.fetchall() return render(request, 'stats.html', {'results': results}) ``` Le query grezze aggirano l'ORM ma sacrificano la portabilità tra database. ## Analizzare le query con django-debug-toolbar Lo strumento django-debug-toolbar consente di visualizzare tutte le query SQL generate da una view Django. ```python # settings.py - Configurazione della debug toolbar INSTALLED_APPS = [ # ... altre app 'debug_toolbar', ] MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', # ... altri middleware ] # Mostra la toolbar per le richieste locali INTERNAL_IPS = ['127.0.0.1'] DEBUG_TOOLBAR_PANELS = [ 'debug_toolbar.panels.sql.SQLPanel', # Query SQL 'debug_toolbar.panels.timer.TimerPanel', # Tempo di esecuzione 'debug_toolbar.panels.cache.CachePanel', # Cache ] ``` Questa configurazione attiva i pannelli più utili per l'ottimizzazione. ```python # Logging delle query SQL in sviluppo # settings.py LOGGING = { 'version': 1, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'level': 'DEBUG', 'handlers': ['console'], }, }, } ``` Questo logging mostra ogni query SQL nella console, utile per individuare problemi N+1. ## Mettere in cache i QuerySet Per dati consultati di frequente e raramente modificati, la cache evita query ripetitive. ```python # views.py from django.core.cache import cache from django.views.decorators.cache import cache_page def list_featured_books(request): """Recupera i libri in evidenza con cache.""" cache_key = 'featured_books_list' # Tentativo di recupero dalla cache books = cache.get(cache_key) if books is None: # Cache miss: eseguire la query books = list( Book.objects.select_related('author') .filter(featured=True) .order_by('-published_date')[:10] ) # Memorizzare in cache per 5 minuti cache.set(cache_key, books, timeout=300) return render(request, 'books/featured.html', {'books': books}) # Decoratore per cachare l'intera view @cache_page(60 * 15) # Cache 15 minuti def list_all_tags(request): """Elenca tutti i tag (dati raramente modificati).""" tags = Tag.objects.annotate( book_count=Count('books') ).order_by('-book_count') return render(request, 'tags/list.html', {'tags': tags}) ``` La cache riduce il carico sul database per dati statici. ## Conclusione L'ottimizzazione delle query con il Django ORM si fonda su pochi principi essenziali: ✅ **Misurare prima di ottimizzare**: usare django-debug-toolbar per individuare i veri problemi ✅ **Eliminare il problema N+1**: `select_related` per ForeignKey, `prefetch_related` per ManyToMany ✅ **Limitare i dati**: `only()`, `defer()`, `values()` per caricare solo ciò che serve ✅ **Indicizzare con criterio**: creare indici sulle colonne filtrate o ordinate spesso ✅ **Cache strategica**: i dati raramente modificati traggono vantaggio dalla cache di Django ✅ **Query grezze**: come ultima risorsa per ottimizzazioni specifiche del database Applicate con metodo, queste tecniche trasformano un'applicazione Django lenta in un sistema performante capace di gestire grandi volumi di dati. Il Django ORM resta uno strumento potente quando le sue sottigliezze vengono padroneggiate. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/django/django-orm-optimizing-queries