Analyzers Overview¶
django-query-doctor ships with seven built-in analyzers. Each one targets a specific category of ORM performance issue, receives fingerprinted query data captured during a request, and returns Prescription objects that describe the problem, its severity, and the exact code change needed to fix it.
How Analyzers Work¶
The analysis pipeline follows four stages:
- INTERCEPT -- the middleware installs an
execute_wrapperthat records every SQL query together with its Python stack trace. - FINGERPRINT -- each captured query is normalized (literal values replaced with placeholders) and hashed so that structurally identical queries share a single fingerprint.
- ANALYZE -- every enabled analyzer receives the full list of fingerprinted queries and inspects them for a specific issue pattern.
- REPORT -- the collected
Prescriptionobjects are handed to a reporter (console, JSON, or HTML) for output.
Each analyzer subclasses BaseAnalyzer and implements a single method:
class BaseAnalyzer(ABC):
@abstractmethod
def analyze(self, queries: list[CapturedQuery]) -> list[Prescription]:
"""Examine captured queries and return prescriptions for any issues found."""
...
A Prescription contains:
| Field | Type | Description |
|---|---|---|
issue_type |
IssueType |
Enum: N_PLUS_ONE, DUPLICATE_QUERY, MISSING_INDEX, etc. |
severity |
Severity |
Enum: CRITICAL, WARNING, or INFO |
description |
str |
Human-readable description of the problem |
fix_suggestion |
str |
The exact code fix as a string |
callsite |
CallSite |
Source file path, line number, and function name |
query_count |
int |
Number of queries involved in this issue |
time_saved_ms |
float |
Estimated time savings if the fix is applied |
fingerprint |
str |
SHA-256 fingerprint of the query group |
extra |
dict |
Additional metadata (e.g., table name, field name) |
Built-in Analyzers¶
| Analyzer | Detects | Default Severity | Documentation |
|---|---|---|---|
| N+1 Query | Repeated queries caused by accessing related objects inside loops | CRITICAL / WARNING | nplusone.md |
| Duplicate Query | Identical SQL executed multiple times within the same request | WARNING | duplicate.md |
| Missing Index | Filters, ordering, or grouping on columns that lack a database index | INFO | missing-index.md |
| Fat SELECT | Selecting all columns when only a subset is used | INFO | fat-select.md |
| QuerySet Evaluation | Unintended queryset evaluation patterns such as len() vs .count() |
WARNING | queryset-eval.md |
| DRF Serializer | N+1 queries originating from Django REST Framework serializer nesting | WARNING | drf-serializer.md |
| Query Complexity | Overly complex SQL with excessive JOINs, subqueries, or aggregations | WARNING / CRITICAL | query-complexity.md |
Disabling Specific Analyzers¶
By default all analyzers are enabled. To disable one or more, set
QUERY_DOCTOR_DISABLED_ANALYZERS in your Django settings:
# settings.py
QUERY_DOCTOR = {
"DISABLED_ANALYZERS": [
"query_doctor.analyzers.fat_select.FatSelectAnalyzer",
"query_doctor.analyzers.query_complexity.QueryComplexityAnalyzer",
],
}
You can also disable analyzers on a per-request basis using the
@diagnose decorator or the diagnose_queries() context manager:
from query_doctor.decorators import diagnose
@diagnose(disabled_analyzers=["FatSelectAnalyzer"])
def my_view(request):
...
Custom Analyzer Plugins¶
You can write your own analyzer by subclassing BaseAnalyzer and registering it
via the QUERY_DOCTOR["EXTRA_ANALYZERS"] setting. See the
Custom Plugins Guide for a full walkthrough.