Django e PostgreSQL nel 2026: indicizzazione, ricerca full-text e domande da colloquio
Guida pratica all'ottimizzazione di Django con PostgreSQL: indici B-tree, parziali e covering, ricerca full-text con SearchVector e GIN, più le domande da colloquio del 2026.

L'ottimizzazione di Django con PostgreSQL segna la differenza tra un'applicazione che scala oltre i primi mille utenti e una che si blocca a ogni vista elenco. PostgreSQL include un motore di indicizzazione e un sottosistema di ricerca full-text che la maggior parte dei progetti Django lascia inutilizzati, sprecando prestazioni sulle query che non costa nulla recuperare. Questa guida illustra i pattern di indicizzazione, gli strumenti di ricerca di django.contrib.postgres e le domande da colloquio sui database che distinguono gli ingegneri Django di livello intermedio da quelli senior nel 2026.
Ogni indice accelera le letture ma rallenta le scritture e occupa spazio su disco. Conviene eseguire EXPLAIN (ANALYZE, BUFFERS) sulla query reale, aggiungere l'indice e poi confrontare il piano. Un indice che il pianificatore non sceglie mai è puro sovraccarico su ogni INSERT e UPDATE.
Fondamenti dell'indicizzazione PostgreSQL in Django
Per impostazione predefinita, PostgreSQL crea un indice B-tree solo sulle chiavi primarie e sulle colonne con unique=True. Ogni altra colonna filtrata o ordinata esegue una scansione sequenziale finché non esiste un indice. Django espone gli indici tramite Meta.indexes, preferibile al più vecchio db_index=True perché consente di definire in un unico punto indici compositi, parziali e covering.
L'ordine delle colonne all'interno di un indice composito non è un dettaglio estetico. Un B-tree può usare un indice per una query solo quando il filtro sfrutta un prefisso iniziale delle colonne indicizzate, una regola spiegata in dettaglio su Use The Index, Luke. Un indice su (status, created_at) serve per WHERE status = ? ORDER BY created_at, ma lo stesso indice non può accelerare una query che filtra solo su created_at.
# models.py
from django.db import models
class Order(models.Model):
customer_email = models.EmailField()
status = models.CharField(max_length=20)
total = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# B-tree a colonna singola per lookup esatti e per intervallo su status
models.Index(fields=["status"]),
# Indice composito: serve per WHERE status = ? ORDER BY created_at DESC
# La colonna iniziale (status) deve comparire nel filtro per essere usata
models.Index(fields=["status", "-created_at"]),
]L'indice composito trasforma una query di dashboard come "ultimi ordini in sospeso" in una singola scansione d'indice, invece di un filtro seguito da ordinamento sull'intera tabella.
Indici parziali e covering per query mirate
Un indice parziale copre solo le righe che soddisfano una condizione, quindi rimane piccolo e resta in memoria. Quando la maggior parte delle righe condivide un valore e le query interessano solo la minoranza, un indice parziale è molto più economico di uno completo. Un indice covering va oltre: aggiungendo colonne non chiave con include, PostgreSQL risponde alla query direttamente dall'indice senza accedere allo heap della tabella, un pattern di accesso chiamato index-only scan.
# models.py
from django.db import models
from django.db.models import Q
class Order(models.Model):
# campi definiti come sopra
class Meta:
indexes = [
# Indice parziale: indicizza solo le righe pending, ignorando lo storico completato
models.Index(
fields=["created_at"],
name="pending_orders_idx",
condition=Q(status="pending"),
),
# Indice covering: total viene letto direttamente dall'indice (index-only scan)
models.Index(
fields=["status"],
include=["total"],
name="status_total_covering_idx",
),
]Su una tabella di ordini in cui il 2% delle righe è in stato pending, l'indice parziale può risultare cinquanta volte più piccolo di un indice completo sulla stessa colonna, e il pianificatore lo mantiene caldo nella buffer cache. La clausola include è documentata in PostgreSQL index-only scans ed è il vantaggio più trascurato nelle applicazioni Django che aggregano una colonna numerica dietro un filtro di stato.
Ricerca full-text in Django con PostgreSQL
Ricorrere a title__icontains=query sembra comodo, ma impone una scansione sequenziale e non è in grado di ordinare i risultati per rilevanza. La ricerca full-text di Django incapsula i tipi nativi tsvector e tsquery di PostgreSQL tramite SearchVector, SearchQuery e SearchRank. Un vettore di ricerca normalizza il testo in lessemi, eliminando le stop word e riducendo le parole alla loro radice, così che una ricerca di "running" corrisponda a "run" e "ran".
# views.py
from django.contrib.postgres.search import (
SearchVector, SearchQuery, SearchRank,
)
from .models import Article
def search_articles(query_text):
query = SearchQuery(query_text, config="english")
# Il peso A classifica le corrispondenze nel titolo sopra quelle nel corpo (peso B)
vector = (
SearchVector("title", weight="A")
+ SearchVector("body", weight="B")
)
return (
Article.objects.annotate(rank=SearchRank(vector, query))
.filter(rank__gte=0.1)
.order_by("-rank")
)Il parametro weight assegna livelli di priorità da A (il più alto) a D. Una corrispondenza nel titolo viene classificata sopra una nel corpo anche quando entrambe contengono il termine cercato, in linea con il comportamento che gli utenti si aspettano da una ricerca. L'API completa e il modello di ponderazione a quattro livelli sono descritti nella documentazione della ricerca full-text di Django.
Indici GIN e SearchVectorField per ricerche veloci
La query precedente ricalcola il vettore di ricerca per ogni riga a ogni richiesta, cosa accettabile per qualche migliaio di righe ma inaccettabile per un milione. Il pattern di produzione memorizza il vettore in un SearchVectorField e lo indicizza con un indice GIN, il tipo di indice che PostgreSQL usa per valori composti come i documenti full-text e i campi JSONB.
# models.py
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
# Documento di ricerca memorizzato e pre-calcolato
search_vector = SearchVectorField(null=True)
class Meta:
indexes = [
# GIN trasforma le ricerche full-text in scansioni d'indice logaritmiche
GinIndex(fields=["search_vector"]),
]Il campo memorizzato deve restare sincronizzato con title e body. Un segnale post_save che scrive il vettore con .update() evita di attivare il segnale in modo ricorsivo, poiché QuerySet.update() non emette segnali di salvataggio.
# signals.py
from django.contrib.postgres.search import SearchVector
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article
@receiver(post_save, sender=Article)
def sync_search_vector(sender, instance, **kwargs):
Article.objects.filter(pk=instance.pk).update(
search_vector=SearchVector("title", weight="A")
+ SearchVector("body", weight="B"),
)Per i team su Django 5.0 o versioni successive, un GeneratedField calcolato dal database o un trigger PostgreSQL elimina del tutto l'UPDATE aggiuntivo, mantenendo la colonna all'interno del database. Qualunque sia il meccanismo che la mantiene popolata, la query filtra poi direttamente il campo indicizzato, e l'indice GIN trasforma quella che era una scansione completa della tabella in una ricerca logaritmica.
Gli indici GIN accelerano le letture a un costo reale in scrittura, poiché ogni inserimento tocca molte voci dell'indice. Sulle tabelle con molte scritture conviene impostare il parametro di storage fastupdate e monitorare la dimensione della pending list, altrimenti le scritture full-text rimarranno bloccate dietro la manutenzione dell'indice.
Scegliere il tipo di indice PostgreSQL corretto
Il B-tree è l'impostazione predefinita corretta, ma PostgreSQL espone diversi tipi di indice tramite django.contrib.postgres.indexes, e sceglierne uno sbagliato spreca spazio su disco senza migliorare una singola query. La scelta dipende dalla forma dei dati e dall'operatore usato dalla query, come catalogato nella documentazione dei tipi di indice di PostgreSQL.
| Tipo di indice | Ideale per | Classe Django |
|------------|----------|--------------|
| B-tree | Uguaglianza e intervalli su colonne scalari | models.Index |
| GIN | Ricerca full-text, JSONB, contenimento in array | GinIndex |
| BRIN | Tabelle enormi append-only ordinate per una colonna | BrinIndex |
| Hash | Lookup di sola uguaglianza su colonne grandi | HashIndex |
BRIN merita un'attenzione particolare per i dati di tipo time-series. Su una tabella di decine di milioni di righe inserite in ordine di timestamp, un BrinIndex su created_at occupa pochi kilobyte laddove un B-tree ne richiederebbe centinaia di megabyte, perché BRIN memorizza solo il valore minimo e massimo per ciascun intervallo di blocchi. Il compromesso è che BRIN aiuta solo quando l'ordine fisico delle righe segue la colonna indicizzata, condizione che vale naturalmente per log append-only e tabelle di eventi.
# models.py
from django.contrib.postgres.indexes import BrinIndex
from django.db import models
class Event(models.Model):
payload = models.JSONField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# Indice minuscolo per scansioni per intervallo su una tabella append-only ordinata per tempo
BrinIndex(fields=["created_at"]),
]Abbinare il tipo di indice al pattern di accesso è un tema ricorrente nei colloqui per posizioni senior, perché dimostra che il candidato ragiona sul layout di archiviazione invece di aggiungere B-tree ovunque per riflesso.
Pronto a superare i tuoi colloqui su Django?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Domande da colloquio Django sui database nel 2026
Le domande sui database dominano i colloqui Django senior perché la padronanza dell'ORM non implica la comprensione di ciò che l'ORM genera. Le domande che seguono riflettono ciò che le commissioni di selezione indagano davvero nel 2026 e si abbinano naturalmente al tuning a livello di query trattato nella guida all'ottimizzazione delle query con l'ORM di Django.
Quando un indice composito su (a, b) non aiuta una query? Quando la query non filtra su a. Un B-tree è ordinato prima per la sua colonna iniziale, quindi un filtro solo su b non può usare l'indice. Questa è la regola del prefisso più a sinistra, e i candidati che la spiegano dimostrano di comprendere la struttura degli indici anziché ricette imparate a memoria.
Qual è la differenza tra select_related e prefetch_related? select_related esegue una JOIN SQL e funziona per relazioni a chiave esterna e uno-a-uno in un'unica query. prefetch_related esegue una seconda query e unisce i risultati in Python, cosa necessaria per le relazioni molti-a-molti e per le chiavi esterne inverse. Scegliere quello sbagliato fa perdere l'ottimizzazione oppure innesca una seconda query non necessaria.
In cosa differisce icontains dalla ricerca full-text a livello di database? icontains si traduce in ILIKE '%term%', che non può usare un normale indice B-tree e scansiona ogni riga. La ricerca full-text confronta con un tsvector pre-calcolato supportato da un indice GIN e ordina i risultati per rilevanza. La distinzione è un segnale affidabile che il candidato è andato oltre i dataset giocattolo.
Perché count() può essere lento su una tabella di grandi dimensioni e quali sono le alternative? PostgreSQL non conserva un conteggio delle righe memorizzato per una tabella, quindi COUNT(*) scansiona ogni riga visibile secondo l'MVCC. Per conteggi approssimati, interrogare pg_class.reltuples restituisce una stima all'istante, mentre per la paginazione la keyset pagination su una colonna indicizzata evita del tutto il conteggio. Una pratica strutturata su questi pattern è disponibile nel modulo di colloquio sul caching in Django, dove i contatori basati su cache sono un tema ricorrente, e in tutto il percorso di apprendimento Django.
Cosa rivela EXPLAIN ANALYZE che EXPLAIN da solo non mostra? EXPLAIN stampa il costo stimato dal pianificatore; EXPLAIN ANALYZE esegue effettivamente la query e riporta tempi e conteggi di righe reali. Uno scarto ampio tra righe stimate ed effettive indica statistiche di tabella obsolete, correggibili con ANALYZE, ed è una causa comune del fatto che il pianificatore ignora un indice che dovrebbe ovviamente applicarsi.
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Conclusione
- Definire gli indici in
Meta.indexesanziché condb_index=True, così che indici compositi, parziali e covering risiedano in un unico punto verificabile. - Ordinare le colonne degli indici compositi secondo la regola del prefisso più a sinistra: la colonna iniziale deve comparire nel filtro della query perché l'indice venga applicato.
- Usare indici parziali quando le query interessano solo una minoranza di righe, come gli ordini in sospeso in una tabella dominata dallo storico completato.
- Aggiungere indici covering con
includeper abilitare gli index-only scan ed evitare l'accesso allo heap nelle query aggregate a forte carico di letture. - Sostituire la ricerca con
icontainsconSearchVector,SearchQueryeSearchRankper ottenere gratuitamente l'ordinamento per rilevanza e lo stemming. - Memorizzare il documento di ricerca in un
SearchVectorFieldsupportato da unGinIndex, mantenuto sincronizzato da un segnale, da unGeneratedFieldo da un trigger del database, prima che la ricerca full-text raggiunga la scala di produzione. - Convalidare sempre le decisioni sugli indici con
EXPLAIN (ANALYZE, BUFFERS); un indice che il pianificatore non seleziona mai è una tassa in fase di scrittura senza alcun beneficio in fase di lettura.
Tag
Condividi
Articoli correlati

Django async view e ASGI nel 2026: performance e domande da colloquio
Un'analisi approfondita delle async view di Django e di ASGI nel 2026: come funzionano internamente, quale server usare in produzione, l'ORM asincrono e la trappola SynchronousOnlyOperation, oltre alle domande da colloquio.

Django 6.0 nel 2026: Chiavi Primarie Composite, Background Tasks e Domande da Colloquio
Analisi tecnica approfondita di Django 6.0: chiavi primarie composite con CompositePrimaryKey, framework nativo per task in background, template partials, middleware CSP integrato e domande da colloquio per sviluppatori Python nel 2026.

Django e Celery: Elaborazione Asincrona dei Task e Domande da Colloquio 2026
Guida completa a Django e Celery: configurazione, task asincroni, code, Celery Beat, monitoraggio e domande frequenti nei colloqui tecnici 2026.