Django: bulk_create, bulk_update e update() con F per scritture veloci
2026-10-16
Importare 5.000 righe con un ciclo di create() o aggiornarle con save() è il secondo classico collo di bottiglia dopo le query N+1: ogni riga costa una query. Django offre tre strumenti per evitarlo. Qui li misuriamo su un esempio eseguibile.
Il modello
class Prodotto(models.Model):
nome = models.CharField(max_length=50)
prezzo = models.IntegerField(default=0)
Inserimenti
# lento: una INSERT per riga
for i in range(5000):
Prodotto.objects.create(nome=f"p{i}", prezzo=i)
# veloce: INSERT a blocchi
Prodotto.objects.bulk_create(
[Prodotto(nome=f"p{i}", prezzo=i) for i in range(5000)]
)
Aggiornamenti
# lento: una UPDATE per riga
for p in Prodotto.objects.all():
p.prezzo += 1
p.save()
# a blocchi, utile se il nuovo valore dipende da logica Python
ps = list(Prodotto.objects.all())
for p in ps:
p.prezzo += 1
Prodotto.objects.bulk_update(ps, ["prezzo"], batch_size=500)
# una sola query, se il calcolo si esprime in SQL
Prodotto.objects.update(prezzo=models.F("prezzo") + 1)
Misure
Eseguite con Django 4.2 su SQLite in memoria, 5.000 righe (tempi indicativi, variano da run a run; il rapporto è ciò che conta):
| Operazione | Query | Tempo |
|---|---|---|
create() in ciclo |
5000 | ~2,9 s |
create() in ciclo dentro transaction.atomic() |
5002 | ~1,7 s |
bulk_create |
13 | ~0,2 s |
save() in ciclo |
5001 | ~4,2 s |
bulk_update, batch 500 |
19 | ~1,6 s |
update(F()) |
1 | ~0,006 s |
Le 13 query di bulk_create non sono un errore: SQLite accetta al massimo 999 parametri per query, quindi Django divide in blocchi da circa 499 righe (2 campi ciascuna). Su PostgreSQL i blocchi sono molto più grandi.
Cosa ricordare
update()conF()vince sempre quando il calcolo si fa in SQL: una query, nessun oggetto caricato in memoria.bulk_updateè più lento dibulk_createperché genera unCASE WHENper ogni riga: va bene, ma non aspettarti la stessa accelerazione.- Racchiudere un ciclo in
transaction.atomic()è il minimo indispensabile se non puoi usare le funzioni bulk: qui dimezza il tempo, perché evita un commit per riga. - Limiti di
bulk_create: non chiamasave(), non emette i segnalipre_save/post_save, e con modelli multi-table inheritance non funziona. Con PostgreSQL imposta le chiavi primarie sugli oggetti; con altri database dipende dal backend. - Usa
batch_sizese le righe hanno molti campi, per non superare i limiti di parametri del database. - Duplicati:
bulk_create(..., ignore_conflicts=True)salta i conflitti di unicità;update_conflicts=True(Django 4.1+) fa un upsert.
Il codice completo di misura è nello script di verifica del progetto; ricostruiscilo con il modello sopra e CaptureQueriesContext per contare le query sul tuo caso reale.