Migration squashing is the process of combining a long chain of Django migrations into fewer, consolidated files.
A few ways to squash migrations:
the Django way:
- use
django-admin squashmigrations - it's the official tool, but you might hit circular dependencies that require manual fixes
- use
the "delete all" way (generating migrations from scratch):
- delete all migrations, run
TRUNCATE django_migrations, then regenerate everything - requires halting PR merges and extreme caution when running commands on production databases
- delete all migrations, run
the "linear paths" way:
- this method targets linear paths and avoids deployment nightmares
This post is mostly a summary of the "linear path" way described in Squashing Django Migrations the Easy Way by Jack Linke.
Visualize the migration graph
Django's migration system builds a Directed Acyclic Graph (DAG), where:
- each migration depends on one or more earlier migrations
- there are no cycles (or loops)
- some migrations are linear (squashable)
- some migrations branch or merge (not squashable)
To see this graph, use the migrationgraph management command:
pip install django-model-info
python manage.py migrationgraph
Identify safe vs unsafe paths
You want to squash only linear sequences that:
- depend on exactly one previous migration
- are depended on by only one subsequent migration
Safe squashable paths are linear:
Avoid unsafe migrations that would break the dependency graph:
- migrations with multiple dependencies, e.g.,
orders/0001 - migrations that depend on multiple migrations, e.g.,
orders/0012
Squash the safe migrations
Run the squashmigrations command for safe paths:
python manage.py squashmigrations orders 0002 0006
python manage.py squashmigrations orders 0007 0011
python manage.py squashmigrations invitations 0002 0003
Each new migration file:
- replaces the specified old migrations
- keeps track of what it replaces via the
replaces = [...]attribute - can be deployed alongside the originals, allowing for a smooth rollout
- is optimized by Django to avoid redundant operations
Deploy in two stages
deploy the squashed migrations, but keep the old ones
- run the squashed migrations
- verify everything runs without error
check that all replaced migrations are marked as applied
SELECT app, name, applied FROM django_migrations WHERE app IN ('orders', 'invitations') ORDER BY app, name;
transition fully to squashed migrations
- delete the migrations that were replaced
- update dependencies in any migrations that relied on them
- swap filenames to the squashed version in the
dependenciesattribute
- swap filenames to the squashed version in the
- remove the
replaces = [...]line from the squashed migration- this converts it into a standard migration
A note on elidable=True
You can mark RunSQL or RunPython operations as elidable by passing elidable=True:
migrations.RunPython(migrate_data_forward, migrations.RunPython.noop, elidable=True)
It's useful for:
- one-off data fixes
- temporary updates that don't need to persist in the final migration history
Django treats elidable operations as safe to omit when squashing.
If you're squashing old migrations that include RunPython or RunSQL, re-check whether they can safely be marked as elidable.