How does Django's squashmigrations work, and when is it safe to delete the migration files a squashed migration replaces?
answer
- one file stands in for many
- the replaces list
- all applied or none applied
- every environment first
basics
~20 ssquashmigrations concatenates an app's migrations, optimizes the operations and writes one migration whose replaces lists the originals. Keep the old files until every database has applied them all; then delete them, repoint dependencies and remove replaces.
solid answer
~40 s`squashmigrations orders 0040` gathers `orders` migrations up to 0040, runs the optimizer over their operations, and writes `0001_squashed_0040_...` (or your `--squashed-name`) with a `replaces` list naming every original. Django then chooses per database: if all or none of the replaced migrations are applied, it uses the squashed one, and `migrate` records it as applied once all originals are; if only some are applied, it keeps using the old files. So you commit the squash **alongside** the old files, release, and wait until every environment, including every developer database and any installs you do not control, has applied through 0040. Then you delete the old files, update migrations that depend on them to depend on the squashed one, remove `replaces`, and can run `migrate --prune` to drop stale rows.
code
bash · 6 linespython manage.py squashmigrations orders 0040 --squashed-name orders_baseline
# writes orders/migrations/0001_orders_baseline.py with replaces = [0001 ... 0040]
# later, once every database has applied 0040:
# delete 0001_initial.py ... 0040_*.py, repoint dependencies, remove replaces
python manage.py migrate --prune ordersgo deeper
Know that squashmigrations folds many migrations into one and that the old files stay until everyone has applied them.
Explain replaces, the all-or-none rule for choosing old versus squashed files, and how the new file is named.
Plan the two-release transition: ship squash with old files, confirm every environment is past the range, then delete, repoint, remove replaces and prune.
Decide how often to squash, weighing faster fresh setups and fewer dead references against the coordination cost across environments and consumers.
## Why squash at all A long-lived app accumulates hundreds of migrations. Django handles that fine at runtime, but fresh databases and test runs replay every operation, and old migrations keep references to code that may no longer exist. **Squashing** replaces a run of migrations with one equivalent migration. ## What the command does `python manage.py squashmigrations app_label [start_migration_name] migration_name`: 1. Collects the app's migrations up to and including `migration_name`, starting at `start_migration_name` if given, otherwise from the first. 2. Concatenates their operations and keeps external dependencies. 3. Runs the **optimizer**, which folds `CreateModel` plus later `AddField`s together, cancels `AddField` followed by `RemoveField`, and so on. `--no-optimize` skips this if the optimizer produces a broken result. 4. Writes the new file: `0001_squashed_0040_<name>` without a start, `<start>_squashed_<end>` with one, or a custom name via `--squashed-name`. 5. Sets `replaces = [("orders", "0001_initial"), ..., ("orders", "0040_...")]` on it. It prints a reminder: commit the new migration but leave the old ones in place until every instance has applied the migrations you squashed. ## How Django picks between old and new | Replaced migrations applied on this database | What Django uses | Recorded state | |---|---|---| | None (fresh database) | the squashed migration | squashed recorded when it runs | | All | the squashed migration, already satisfied | squashed recorded as applied on the next `migrate` | | Some | the original files, continuing where it stopped | squashed recorded once the rest are applied | When the squashed migration is used, dependencies elsewhere that point at a replaced migration are remapped to it while `replaces` is present. This all-or-none rule is what lets old and new files coexist safely. ## When the old files can go Only when **every database the code will ever meet** has applied all the replaced migrations: - production, staging and review environments; - every developer database and CI cache that is not rebuilt from scratch; - for a reusable app, every user installation, which means shipping a release that contains both old and new files and requiring upgrades to pass through it. If you delete early, a database stuck in the middle of the squashed range has neither its remaining original files nor a usable squash. ## Transitioning to a normal migration 1. Delete all migration files listed in `replaces`. 2. Update any migration, in this app or others, whose `dependencies` name a deleted file so it depends on the squashed migration. 3. Remove the `replaces` attribute; without it, the file is an ordinary migration. 4. Optionally run `migrate --prune orders` so `django_migrations` loses rows for files that no longer exist. ## Limits worth knowing - `RunPython` and `RunSQL` block the optimizer unless marked `elidable=True`, and their functions must be copied into the squashed file by hand. - Squashing can surface a `CircularDependencyError` between apps; the documented fix is to split out one foreign key into a separate migration. - Since Django 6.0 a squashed migration can itself be squashed before it has been transitioned; before 6.0 you had to transition it first.
- A squashed migration is deployed to a database that had applied every replaced migration; does migrate run anything?No operations run. On every `migrate`, Django marks a squashed migration as applied when all the migrations it replaces are recorded, so that database simply gains a record for the squashed migration.
- Why would you pass a start_migration_name to squashmigrations?To squash only a later range, for example everything after the last `RunPython`, so the squash avoids functions that need manual copying and operations that block the optimizer. The squashed file is then named after the start and end migrations.
saying these in an interview costs you the question
- Delete the old files right after running squashmigrations
- A partially migrated database switches to the squashed migration
- You must fake the squashed migration on every existing database
- Squashing changes the schema of existing databases
- The replaces attribute should stay forever