Analyzers Overview¶
django-query-doctor ships with eight 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 |
| SerializerMethodField | N+1 queries in DRF SerializerMethodField methods (static AST analysis) |
WARNING | drf-serializer.md |
| Query Complexity | Overly complex SQL with excessive JOINs, subqueries, or aggregations | WARNING / CRITICAL | query-complexity.md |
| Write N+1 | Repeated single-row INSERT/UPDATE/DELETE statements from a loop |
WARNING / CRITICAL | write-nplusone.md |
Disabling Specific Analyzers¶
By default all analyzers are enabled. To disable one, set its enabled flag
under ANALYZERS in your Django settings. The keys are the analyzer config
names: nplusone, duplicate, missing_index, fat_select, queryset_eval,
complexity, serializer_method, and write_nplusone:
# settings.py
QUERY_DOCTOR = {
"ANALYZERS": {
"fat_select": {"enabled": False},
"complexity": {"enabled": False},
},
}
There is no per-request disable mechanism: @diagnose and
diagnose_queries() take no arguments and run every enabled analyzer.
Custom Analyzer Plugins¶
You can write your own analyzer by subclassing BaseAnalyzer and registering
it via the query_doctor.analyzers entry point group in your package's
pyproject.toml. See the Custom Plugins Guide
for a full walkthrough.