Django: only(), defer() e values_list() per leggere meno dati
Di default Model.objects.all() legge tutte le colonne di ogni riga. Se il modello ha un campo di testo lungo e a te serve solo il titolo, stai spostando dati che butterai via.
class Articolo(models.Model):
titolo = models.CharField(max_length=100)
corpo = models.TextField() # 2.000 caratteri per riga nel test
Le tre opzioni
Articolo.objects.only("titolo") # istanze del modello, solo titolo (+ pk)
Articolo.objects.defer("corpo") # istanze del modello, tutto tranne corpo
Articolo.objects.values_list("titolo", flat=True) # niente modelli, solo valori
Misure
20.000 righe, SQLite in memoria, Django 4.2.30, Python 3.9.6, list(...) del queryset, migliore di 5 esecuzioni (script verifica/only_values.py). I tempi variano di run in run, conta l'ordine di grandezza:
all(): circa 970 ms, picco di memoria circa 50 MBonly("titolo"): circa 590 ms, circa 9 MBdefer("corpo"): circa 380 ms, circa 9 MBvalues_list("titolo"): circa 29 ms, circa 2,4 MBvalues_list("titolo", flat=True): circa 57 ms, circa 2,4 MB (nella stessa misura la differenza con la forma a tuple è rumore)
Tutte e cinque eseguono una sola query. only e defer sono equivalenti nel risultato (memoria identica); la differenza di tempo tra i due in questa misura è rumore, non una regola.
La trappola: i campi differiti
Se carichi con only("titolo") e poi tocchi a.corpo, Django fa una query per ogni oggetto:
for a in Articolo.objects.only("titolo")[:200]:
a.corpo
Risultato misurato: 201 query (1 + 200). È un N+1 creato da te. Se non sei sicuro di non toccare quel campo, non differire.
Quale scegliere
- Ti servono solo dei valori (liste, esportazioni,
insu ID, autocomplete):values_list. È la più veloce perché non costruisce istanze del modello. - Ti servono gli oggetti (metodi,
__str__, template) ma non una colonna pesante:defer()sulla colonna pesante, che è il caso più leggibile. - Poche colonne leggere su un modello largo:
only(). - Poche righe e modello piccolo: lascia stare, l'ottimizzazione prematura costa più di quello che risparmia. Misura prima, ad esempio con
CaptureQueriesContextetime.perf_counter.
Se il problema sono le query moltiplicate e non le colonne, vedi il problema N+1.