Adding non-nullable fields, and large data migrations
coreadvancedAdding a non-nullable field to a table with EXISTING rows fails outright — the database has no value to put in those rows. Django's own documented fix is three migrations: add the field nullable with a default, backfill every existing row via a data migration, THEN alter the field to drop null=True. For genuinely large tables, wrapping a big backfill in Django's normal single transaction holds a lock for the whole run — atomic = False on the migration class, combined with processing rows in small batches, avoids that.
Think of it as
A migration that adds a required (non-nullable) field is really asking the database to answer a question it can't answer on its own: "what value should THIS EXISTING row have for a field it never had?" Django can't invent that answer, so the honest fix is to split the single conceptual change into three separate, safe steps — first make the column exist but optional (nullable, or with a default), then go fill in a real value for every existing row, THEN (only once every row genuinely has a value) tighten the constraint to non-nullable. Skipping straight to the tightened constraint is what fails. The batching concern is a separate, second problem — even a WORKING backfill, if written as one giant UPDATE (or one huge Django migration transaction) against a table with millions of rows, can hold a lock for the entire duration, blocking other queries the whole time; atomic=False plus small batches trades one long lock for many short ones, letting other traffic interleave between batches.
What we're doing: Backfill a UUID field on a large table in small batches, each its own transaction, avoiding one long-held lock across the entire table.
- 1
- The while loop processes 1000 rows at a time, repeating until none remain — never holding a lock across the ENTIRE table at once.
- 5
- Each batch of 1000 gets its own transaction.atomic() block — a lock held only for that batch's duration, released before the next batch starts, letting other queries run in between.
Why this works: A naive migration backfilling all rows in one RunPython call, inside Django's default single migration transaction, would hold a lock on every touched row for the ENTIRE backfill's duration — on a table with millions of rows, that could be minutes of blocked writes; batching trades one long lock for thousands of short ones, letting normal application traffic interleave between batches.
Adding a required field directly, without the nullable-then-backfill-then-tighten sequence
Wrong
Better
What you see: django.db.utils.IntegrityError (or a similar constraint-violation error) raised the moment migrate runs, on any table that already has rows — the migration cannot proceed at all.
Why: A non-nullable field with no default has no value the database can use for rows that already existed before the column did — the database is not being unreasonable here, there genuinely is no answer to "what should this existing row's new required field be" until something (a backfill) supplies one, which is exactly why the safe pattern splits this into three separate steps instead of one.
- 1. Add, nullable — AddField(..., null=True) — the column exists, nothing required yet
- 2. Backfill — a data migration sets a real value on every existing row
- 3. Tighten — AlterField(..., null=False) — safe now that every row has a value
The three-migration pattern for a non-nullable field
Together
Remember: Adding a non-nullable field to a table with existing rows needs three migrations: add nullable, backfill via a data migration, then tighten to non-nullable — never all in one step. atomic = False plus small per-batch transactions avoids holding one long lock across a large backfill. Rows created in the gap between "added" and "backfilled" can still need explicit handling.
See also: the expand and contract technique · data migrations fake and squashing · locks and deadlocks

