django-fastmig
Experimental. fastmig is a drop-in add-on for Django (tested with 4.2, 5.2
and 6.0) that makes migrate on large projects several times faster without
changing the SQL that gets executed.
pip install django-fastmig
INSTALLED_APPS = ["fastmig", *INSTALLED_APPS] # or: import fastmig; fastmig.install()
FASTMIG_DISABLE=1 turns it off. Pure Python, no settings, no changes to your
migrations, models or database.
Results
Fresh database; every DDL statement the schema editor executes is logged and compared between the two runs.
| project | models / migrations | backend | stock | fastmig | speedup | DDL |
|---|---|---|---|---|---|---|
| "Project A" — real in-house inventory/ERP app (taggit, modeltranslation, auditlog, pgtrigger, 49 data migrations) | 222 / 493 | PostgreSQL 15 | 154–178 s | 17–22 s | 8–9.5× | identical (22 557 statements) |
Project A, pytest --create-db, one app |
PostgreSQL 15 | 170 s | 23 s | 7× | ||
Project A, reverse: migrate <app> zero for all 22 apps |
222 / 493 | PostgreSQL 15 | 851 s | 39 s | 21× | identical (15 039 statements) |
| generated, 400 models | 400 / 818 | SQLite | 313–364 s | 57 s | 5.5–6.4× | identical (11 262) |
| generated, 120 models | 120 / 223 | SQLite / PostgreSQL | 17–20 s | 4–5 s | 3.5–4.6× | identical |
generated corner cases (MTI, proxies, O2O pks, to_field, explicit through, renames, pk type changes) |
53 / 59 | SQLite / PostgreSQL | 1.5 / 3.0 s | 0.7 / 2.2 s | 2× / 1.4× | identical |
Reverse migrations gain the most: before unapplying, Django recomputes the project state for every migration in the plan by cloning and mutating it, which repeats the whole re-rendering cost on top of the forward pass.
What remains is mostly the database executing DDL and the class renders Django
still needs (including a full re-render before every RunPython, same as
stock). Scripts and generators are in benchmarks/.
Django's own test suite passes with fastmig installed: the migrations and
schema suites (1 033 tests) on both SQLite and PostgreSQL, plus 1 435 tests
from related labels (migrate_signals, contenttypes_tests, auth_tests,
proxy_models, model_inheritance, swappable_models, backends, …).
Why migrate is slow
The schema editor works on rendered model classes. After every operation
Django rebuilds ("fakes") classes with type(...) — expensive pure-Python work
(ModelBase.__new__, field cloning via deconstruct(), contribute_to_class,
M2M through models). For any relational change stock Django rebuilds the
model's entire connected component; on a connected schema that is
O(models) class builds per operation. The 120-model project above rebuilds
37 080 classes over 223 migrations; fastmig rebuilds 1 163. See the forum
thread Very slow migrations with large numbers of tables.
How fastmig differs from the "shallow reload" patch in that thread
That patch (SHALLOW_RELOAD_MIGRATION) reloads only the mutated model. It gets
the speed, but models that point at the re-rendered one keep pointing at the
old class object — reverse relations and FK targets can land on the wrong
class, which is what Django's maintainers warned about. It usually works
because schema editors mostly read table/column names, which are the same on
the stale class.
fastmig:
- re-renders the mutated model plus what genuinely needs a new class:
subclasses/proxies, targets of added/removed/altered relations, and — when a
referenced field, pk or relation changed — direct predecessors, propagating
through pk-type dependencies (MTI, O2O primary keys,
to_field); - re-links every other model's forward pointers to the new class through
Django's own
do_related_class(), clearing cached properties and following auto-created through tables; - preserves the pre-change class that
RenameModel/AlterFieldon a pk read fromfrom_state(stock only gets it by accident of traversal order); - unregisters stale auto-created through classes;
- re-renders instead of re-linking for third-party relation fields it doesn't
know (e.g. taggit's
TaggableManager); - keeps Django's
from_statecontract (#24225, #24573).
The plan is computed from ModelState objects, never from rendered classes.
What it patches
fastmig.install() (run by the AppConfig) monkey-patches
django.db.migrations.state.ProjectState: reload_model/reload_models
(planner + re-link), add_field/remove_field/alter_field/add_model/
rename_model/remove_model (hints about what changed), and clone
(cheaper, equivalent). uninstall() restores everything. fastmig.stats
counts reloads, renders and re-linked fields.
Caveats
- Validated on SQLite and PostgreSQL only; not on MySQL/Oracle.
- Third-party operations that mutate a model's
ModelStateand callstate.reload_model()for it (pgtrigger,django.contrib.postgres) work unchanged. Untested: an operation that mutates model X but only worked because stock's component-wide re-render rebuilt X as a side effect of reloading another model. I know of no package doing this. - Some operations emit several independent DDL statements (e.g. altering the FK column in each table referencing a changed pk). Django orders them by model-registry order, which is hash-seed dependent — stock Django itself emits them in varying order between runs. The benchmark scripts treat a pure reordering of otherwise identical statements as a match.
- If anything looks off:
FASTMIG_DISABLE=1, andbenchmarks/bench_real.pygives you a full stock-vs-fastmig DDL diff for your project.
Development
pip install -e . django
python -m unittest discover -s tests -v # unit + SQL parity tests
python benchmarks/gen_project.py benchmarks/demo --apps 8 --models 15 --rounds 30
python benchmarks/bench.py benchmarks/demo [--pg dbname] # stock vs fastmig, DDL diff
python benchmarks/bench_real.py <project> <python> <settings> <pg_db> [runs]
DJANGO_SRC=/path/to/django python benchmarks/run_django_tests.py --settings test_sqlite migrations schema
BSD 3-Clause license.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_fastmig-0.1.1.tar.gz.
File metadata
- Download URL: django_fastmig-0.1.1.tar.gz
- Upload date:
- Size: 16.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94d6b219d570b903ee9ca00dfe83d00ecacc8b2333e199aff5fe3a20e06a5147
|
|
| MD5 |
dd622988537f4d7bfdb084e9377cf0c7
|
|
| BLAKE2b-256 |
52b824859a6700a18f26305fe2f5455cb4b11906ba39ebcc70b16f6e41cac6bc
|
File details
Details for the file django_fastmig-0.1.1-py3-none-any.whl.
File metadata
- Download URL: django_fastmig-0.1.1-py3-none-any.whl
- Upload date:
- Size: 14.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30b6d8a15a347a9ab8723699c198d145017d7fe00c78fa3e08fa522df02a9607
|
|
| MD5 |
7213235c291f242034f82e0141b64a04
|
|
| BLAKE2b-256 |
e3baa5614c30ca8b0530220afcd077ba1547d13a460e2313ae340b10a6da0bf9
|