Skip to content

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:

  1. Analyzes the target URL(s) by executing requests and capturing queries.
  2. Generates prescriptions with exact file paths, line numbers, and fix code.
  3. Parses the target source files using Python's AST module.
  4. 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:

python manage.py fix_queries --url /api/books/

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:

python manage.py fix_queries --url /api/books/ --apply

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. Use git diff to 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

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:

def get_queryset(self):
    return Book.objects.all()
    # Accessing book.author in template triggers N+1

After:

def get_queryset(self):
    return Book.objects.select_related('author').all()

Targets N+1 patterns from ManyToManyField or reverse ForeignKey traversal. The fix adds .prefetch_related('field_name') to the queryset.

Before:

books = Book.objects.all()
for book in books:
    categories = book.categories.all()  # N+1

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:

class Book(models.Model):
    published_date = models.DateField()

After:

class Book(models.Model):
    published_date = models.DateField()

    class Meta:
        indexes = [
            models.Index(fields=["published_date"]),
        ]

Note: After applying meta_index fixes, you must generate and run a migration:

python manage.py makemigrations
python manage.py migrate

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

  1. Always review diffs first. Run without --apply, read every change, then apply.
  2. Commit before applying. Use version control so you can revert if needed.
  3. Apply one fix type at a time. This makes it easier to review and test each change.
  4. Run tests after applying. Ensure your test suite passes after each batch of fixes.
  5. Handle meta_index separately. Index changes require migrations and may affect write performance. Evaluate each one individually.

Further Reading