Auto-Fix¶
django-query-doctor can automatically apply the fixes it prescribes by modifying your Python source files. This page explains how the auto-fix system works, what fix types are supported, and how to use it safely.
How It Works¶
When you run fix_queries, django-query-doctor:
- Analyzes the target URL(s) by executing requests and capturing queries.
- Generates prescriptions with exact file paths, line numbers, and fix code.
- Parses the target source files using Python's AST module.
- Applies the fixes by modifying the AST and writing the updated source back to disk.
The AST-based approach ensures that fixes are syntactically correct and properly placed. django-query-doctor does not use simple string replacement -- it understands the structure of your code.
Dry Run (Default)¶
By default, fix_queries runs in dry-run mode. It shows you a diff of what would change without modifying any files:
Output:
--- myapp/views.py (original)
+++ myapp/views.py (fixed)
@@ -83,7 +83,7 @@
def get_queryset(self):
- return Book.objects.all()
+ return Book.objects.select_related('author').all()
--- myapp/serializers.py (original)
+++ myapp/serializers.py (fixed)
@@ -22,7 +22,11 @@
class Meta:
model = Book
- fields = "__all__"
+ fields = ["id", "title", "isbn", "published_date"]
Review this output carefully before proceeding.
Applying Fixes¶
To modify your source files, pass the --apply flag:
Warning: Always ensure your code is committed to version control before running
--apply. While django-query-doctor generates correct fixes in the vast majority of cases, complex querysets (dynamic construction, conditional chaining, multi-line expressions) may require manual adjustment. Usegit diffto review changes after applying.
Supported Fix Types¶
| Fix Type | What It Does | Example |
|---|---|---|
select_related |
Adds .select_related() calls for FK/OneToOne N+1 patterns |
.select_related('author', 'publisher') |
prefetch_related |
Adds .prefetch_related() calls for M2M/reverse FK N+1 patterns |
.prefetch_related('categories', 'tags') |
only_defer |
Replaces SELECT * with .only() or adds .defer() for unused columns |
.only('id', 'title', 'price') |
meta_index |
Adds models.Index() entry to model's Meta.indexes for missing indexes |
Meta: indexes = [models.Index(fields=["published_date"])] |
cache_queryset |
Extracts repeated queryset evaluations into a variable | books = list(Book.objects.filter(...)) |
Targeting Specific Fix Types¶
You can limit which categories of fixes are applied using --fix-type:
# Only apply select_related fixes
python manage.py fix_queries --url /api/books/ --fix-type select_related --apply
# Only apply index-related fixes
python manage.py fix_queries --url /api/books/ --fix-type meta_index --apply
# Apply multiple specific fix types
python manage.py fix_queries --url /api/books/ \
--fix-type select_related \
--fix-type prefetch_related \
--apply
This is useful when you want to apply safe, well-understood fixes (like select_related) while leaving more complex changes (like meta_index, which requires a migration) for manual review.
Fix Details¶
select_related¶
Targets N+1 patterns caused by accessing ForeignKey or OneToOneField relations in loops. The fix adds .select_related('field_name') to the queryset that feeds the loop.
Before:
After:
prefetch_related¶
Targets N+1 patterns from ManyToManyField or reverse ForeignKey traversal. The fix adds .prefetch_related('field_name') to the queryset.
Before:
After:
books = Book.objects.prefetch_related('categories').all()
for book in books:
categories = book.categories.all() # Uses prefetched cache
only_defer¶
Targets queries that fetch all columns when only a subset is used. The fix adds .only() with the columns that are actually accessed.
Before:
books = Book.objects.filter(published=True)
titles = [book.title for book in books] # Only uses 'title'
After:
books = Book.objects.filter(published=True).only('id', 'title')
titles = [book.title for book in books]
meta_index¶
Targets model fields used in WHERE or ORDER BY clauses that lack a database index. The fix adds a models.Index() entry to the model's Meta.indexes.
Before:
After:
class Book(models.Model):
published_date = models.DateField()
class Meta:
indexes = [
models.Index(fields=["published_date"]),
]
Note: After applying
meta_indexfixes, you must generate and run a migration:
cache_queryset¶
Targets duplicate queries caused by evaluating the same queryset multiple times. The fix extracts the queryset evaluation into a variable.
Before:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["count"] = Book.objects.filter(active=True).count()
ctx["books"] = Book.objects.filter(active=True)[:10] # Same filter, separate query
return ctx
After:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
_qs_books_active = Book.objects.filter(active=True)
ctx["count"] = _qs_books_active.count()
ctx["books"] = _qs_books_active[:10]
return ctx
Best Practices¶
- Always review diffs first. Run without
--apply, read every change, then apply. - Commit before applying. Use version control so you can revert if needed.
- Apply one fix type at a time. This makes it easier to review and test each change.
- Run tests after applying. Ensure your test suite passes after each batch of fixes.
- Handle
meta_indexseparately. Index changes require migrations and may affect write performance. Evaluate each one individually.
Further Reading¶
- Management Commands -- Full command reference.
- How It Works -- Understanding prescriptions and the analysis pipeline.
- CI Integration -- Using auto-fix in CI workflows.