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.

Ottimizzazione delle query del Django ORM per prestazioni massime

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.

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.

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.

Pronto a superare i tuoi colloqui su Django?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

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.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

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.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#django
#django orm
#python
#ottimizzazione database
#prestazioni

Condividi

Articoli correlati