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-run exits 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_migrations table. 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_slugs
from 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, no save() logic you added to the real model.
  • Read in batches with .iterator() and write with bulk_update when 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 = False on the Migration class — and then make it individually idempotent, because a failure leaves it partially applied.
  • Give a reverse function or migrations.RunPython.noop explicitly. A migration with no reverse cannot be unapplied, which you discover at the worst moment.

Changing a live schema safely

ChangeInstead ofDo
Rename a fieldRemoveField + AddField (drops the data)RenameField, or AlterField in one operation
Add a required fieldA non-null column with no default on a big tableAdd null=True, backfill, then AlterField to non-null
Rename a model or appRecreating itRenameModel / RenameContentType, then verify FKs
Drop a columnOne migration that removes it with the releaseShip code that stops using it first, drop it in a later release
Add an indexPlain AddIndex on a busy tablePostgreSQL concurrent index via AddIndexConcurrently, then a regular migration
Change a typeAlterField alone when the cast failsRunSQL 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 table
⚠️
Deployment order is not optional: run migrate 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?
Only if it has never been applied to any shared environment, and even then it is risky: a colleague may already have run it. The reliable route is a new migration that reverses the mistake. Reserve history edits for the branch before the first merge.
Why does makemigrations want to change things I did not touch?
Usually a model option (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.

Settings, caching and query performance Deployment: gunicorn, static files and the security checklist

Last refreshed 2026-09-18.