Django: exists(), count() o len()? Quale usare e quanto costa sbagliare
Tre righe che sembrano equivalenti e che invece generano query molto diverse:
if Ordine.objects.filter(stato="aperto"): # carica tutte le righe
n = len(Ordine.objects.filter(stato="aperto")) # carica tutte le righe
n = Ordine.objects.filter(stato="aperto").count() # SELECT COUNT(*)
Cosa fa ciascuno
if qs:/bool(qs): valuta il queryset, cioè esegue laSELECTcompleta e crea un oggetto Python per ogni riga, solo per sapere se ce n'è almeno una.len(qs): stessa cosa, poi conta gli elementi della lista in memoria.qs.count(): esegueSELECT COUNT(*), senza trasferire righe.qs.exists(): esegueSELECT 1 ... LIMIT 1. Il database si ferma alla prima riga trovata.
Numeri misurati
Tabella con 50.000 righe (campo testo da 200 caratteri), SQLite in memoria, Django 4.2.30, migliore di 7 esecuzioni:
| Istruzione | Query | Tempo |
|---|---|---|
bool(qs) |
1 (tutte le righe) | circa 400 ms |
len(qs) |
1 (tutte le righe) | circa 420 ms |
qs.count() |
1 (COUNT(*)) |
circa 2 ms |
qs.exists() |
1 (LIMIT 1) |
circa 0,5 ms |
Su un database vero la differenza cresce, perché alle righe da creare si aggiunge il trasferimento in rete. Lo script è nel repository del sito (verifica/exists_count.py).
La regola
- Ti serve sapere se esistono righe:
exists(). - Ti serve quante:
count(). - Ti servono le righe: iteri il queryset (o
list(qs)), e solo dopo usilen()oif.
L'eccezione: queryset già caricato
Se hai già valutato il queryset, il risultato è in cache. len(qs) e bool(qs) sono gratis, mentre count() e exists() usano la cache e non fanno altre query. Nel test, list(qs) seguito da bool, len, count e exists produce una sola query in totale.
ordini = Ordine.objects.filter(stato="aperto")
for o in ordini: # una query, risultato in cache
...
if ordini: # nessuna query in più
...
Il problema nasce quando il queryset non è ancora caricato e chiami prima exists() e poi lo iteri: sono due query. Se alla fine userai comunque le righe, evita exists() e itera direttamente, gestendo il caso vuoto con {% empty %} nel template o con un controllo sulla lista.
Nei template
{% if ordini %} valuta tutto il queryset. Se in quella pagina non mostri le righe, passa al template ordini.exists (in un template le parentesi si omettono) oppure calcola il booleano nella view.
Errore frequente
qs.count() > 0 per verificare l'esistenza: funziona, ma exists() è più veloce, perché non deve contare tutte le righe, e dice meglio cosa intendi.