Migrations in depth
How makemigrations and migrate really relate, how to write data migrations, and how to change a schema without dropping a production table.
makemigrations versus migrate
These two commands do completely different jobs, and most migration confusion comes from mixing them up. makemigrations compares your model classes with the previous migration files and writes Python. It never touches the database. migrate reads those files and executes SQL through the database backend. The model files are the intent; the migration files are the history; the database is the result.
python manage.py makemigrations blog # write 0004_post_views.py
python manage.py makemigrations blog --dry-run --verbosity 3 # show the ops first
python manage.py sqlmigrate blog 0004 # print the SQL it will run
python manage.py showmigrations # [X] = applied, [ ] = pending
python manage.py migrate # apply everything pending
python manage.py migrate blog 0003 # migrate back to 0003
python manage.py migrate blog zero # unapply the whole app# blog/migrations/0004_post_views.py (generated, then committed)
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("blog", "0003_post_published_at"),
migrations.swappable_dependency("settings.AUTH_USER_MODEL"),
]
operations = [
migrations.AddField(
model_name="post",
name="views",
field=models.PositiveIntegerField(default=0),
),
migrations.AlterModelOptions(
name="post",
options={"ordering": ["-published_at", "-created_at"]},
),
]- Migration files are source code: commit them with the model change, review them in the pull request, and never edit one that has been applied anywhere.
- Each migration runs in a transaction on backends that support transactional DDL (PostgreSQL, SQLite). MySQL does not, so a failed migration there can leave half-applied state.
makemigrations --check --dry-runexits non-zero when a model has changes with no migration — exactly what you want as a CI guard.- The database records applied migrations in the
django_migrationstable. If a migration file disappears and the row remains, later runs report an inconsistent history.
Data migrations
Schema migrations change structure. Data migrations change rows, and they belong in the same ordered history so every environment reaches the same state. Generate an empty migration with --empty and add a RunPython operation.
python manage.py makemigrations blog --empty --name backfill_slugsfrom django.db import migrations
from django.utils.text import slugify
def backfill_slugs(apps, schema_editor):
Post = apps.get_model("blog", "Post") # the historical model, not your import
for post in Post.objects.filter(slug=""):
post.slug = slugify(post.title)[:200] or f"post-{post.pk}"
post.save(update_fields=["slug"])
def clear_slugs(apps, schema_editor):
apps.get_model("blog", "Post").objects.update(slug="")
class Migration(migrations.Migration):
dependencies = [("blog", "0004_post_views")]
operations = [
migrations.RunPython(backfill_slugs, clear_slugs), # always reverse-able
]- Inside a data migration the historical model from
apps.get_model()has fields only — no custom methods, no manager overrides, nosave()logic you added to the real model. - Read in batches with
.iterator()and write withbulk_updatewhen the table is large; a loop that loads a million rows into memory will time out a deploy. - If the migration must not run in one transaction (huge backfills, index creation), set
atomic = Falseon the Migration class — and then make it individually idempotent, because a failure leaves it partially applied. - Give a reverse function or
migrations.RunPython.noopexplicitly. A migration with no reverse cannot be unapplied, which you discover at the worst moment.
Changing a live schema safely
| Change | Instead of | Do |
|---|---|---|
| Rename a field | RemoveField + AddField (drops the data) | RenameField, or AlterField in one operation |
| Add a required field | A non-null column with no default on a big table | Add null=True, backfill, then AlterField to non-null |
| Rename a model or app | Recreating it | RenameModel / RenameContentType, then verify FKs |
| Drop a column | One migration that removes it with the release | Ship code that stops using it first, drop it in a later release |
| Add an index | Plain AddIndex on a busy table | PostgreSQL concurrent index via AddIndexConcurrently, then a regular migration |
| Change a type | AlterField alone when the cast fails | RunSQL with an explicit USING expression |
python manage.py squashmigrations blog 0001 0009 # after --squashmigrations review
python manage.py migrate --plan # what would run, in order
python manage.py migrate blog 0007 --fake # mark applied without running
python manage.py migrate blog 0007 --fake-initial # adopt a pre-existing tablemigrate before the new code starts serving, and design each migration so the previous release still works with the new schema. A model that loses a column while old workers are still running produces ProgrammingError: column does not exist for every request they handle. Additive changes first, removal in a later release.FAQ
Can I fix a migration after I have already pushed it?
Why does makemigrations want to change things I did not touch?
Meta.ordering, verbose_name) or a field argument has drifted from the recorded state, or two developers generated overlapping migrations. Run it on a clean checkout with --dry-run --verbosity 3 to see the exact difference before committing anything.Related
Settings, caching and query performance Deployment: gunicorn, static files and the security checklist
Last refreshed 2026-09-18.