1.`bulk_create()`, `bulk_update()`, and what they skip
Calling `save()` in a loop over 50,000 objects issues 50,000 statements. `bulk_create()` and `bulk_update()` collapse that into a handful, and `batch_size` controls how many rows go in each one. The price is that they are deliberately dumb: Django's documentation states that "the model's `save()` method will not be called, and the `pre_save` and `post_save` signals will not be sent". Anything your `save()` override does — stamping a field, updating a search vector, writing an audit row — silently does not happen. That is a feature when you want raw speed and a serious bug when you did not know about it.
Key features
- Both skip `save()` and both skip `pre_save`/`post_save` — quoted directly from the QuerySet reference
- `bulk_create` "does not work with child models in a multi-table inheritance scenario" and does not handle many-to-many relations
- Primary keys are set after `bulk_create` on PostgreSQL, MariaDB and SQLite 3.35+ — but `ignore_conflicts` "disables setting the primary key on each model instance"
- `update_conflicts=True` with `update_fields` and `unique_fields` gives an upsert rather than an error
- `bulk_update()` returns the number of objects updated and cannot update the primary key
- `bulk_update()` "prepares all of the WHEN clauses for every object across all batches before executing any queries"
Which write to reach for
- Whole: Order.objects.bulk_create(new_orders, batch_size=500, update_conflicts=True, update_fields=["total"], unique_fields=["reference"])
- bulk_create — skips save() and signals: The documented caveat: "the model's save() method will not be called, and the pre_save and post_save signals will not be sent." Read the model's save() before choosing this.
- new_orders — unsaved instances, built in memory: The list itself is a memory cost. For very large inputs, build and write it in chunks rather than assembling millions of objects first.
- batch_size=500 — rows per statement: Bounds statement size so the database or driver does not reject it. Without it Django uses one statement for everything, which can be very large.
- update_conflicts=True — upsert instead of error: A conflicting row is updated rather than raising `IntegrityError`. This is what makes a re-run of an import safe.
- update_fields=["total"] — what a conflict overwrites: Only these columns are updated on conflict. Omitting a field means an existing row keeps its current value for it — which is usually what you want for `created_at`.
- unique_fields=["reference"] — what counts as a conflict: Must match a real unique constraint on the table. This is the column the database uses to decide "the same row", so it defines what a re-run means.
Command
Order.objects.bulk_create(objs, batch_size=500)
Order.objects.bulk_update(objs, ["total", "state"], batch_size=500)Bulk writes write rows; they do not save objects. `save()`, `pre_save` and `post_save` are all skipped, which is where the speed comes from and where the data-quality bugs come from — so read the model's `save()` and its signal receivers first, then reproduce that work in the loop that builds the objects, in bulk. Use `update_conflicts` with `unique_fields` to make an import re-runnable, and keep `created_at` out of `update_fields`. Bound both sides: `.iterator()` on the read, a flush counter on the write, because `bulk_update` builds every `WHEN` clause before it executes anything.