django-query-doctor v2.0 — SQL compilation cache, AST serializer analysis, baseline regression detection

Hi Django community,

I’m Hassan — a senior backend engineer from Pakistan. A month ago I posted v1.0 of django-query-doctor here. Today I’m sharing v2.0, which went significantly deeper.


What it does

django-query-doctor intercepts Django ORM queries and gives you exact file:line prescriptions — not vague warnings. It detects N+1 queries, duplicate queries, missing indexes, fat SELECTs, queryset misuse, DRF serializer N+1s, and query complexity issues.

pip install django-query-doctor


What’s new in v2.0

QueryTurbo — SQL compilation cache

The headline feature. Django’s SQLCompiler.as_sql() rebuilds the SQL template on every query execution, even when the query structure is identical. QueryTurbo caches the compiled SQL template after fingerprinting the query structure (blake2b hash of model, fields, lookups, JOINs, ordering — never user values), then extracts parameters directly from Django’s internal WhereNode tree, bypassing as_sql() entirely on repeated identical-structure queries.

Three-phase lifecycle:

  • UNTRUSTED: first 3 executions validate cached SQL against fresh as_sql() output
  • TRUSTED: as_sql() skipped entirely, params extracted from Query tree
  • POISONED: SQL mismatch detected → entry blacklisted permanently (survives cache.clear())

On PostgreSQL with psycopg3, it additionally uses protocol-level prepared statements.

Important caveat: the speedup applies to SQL compilation overhead only, not total query time including database round-trip. On SQLite in-memory, fingerprinting overhead actually makes it ~35% slower end-to-end. The benefit is meaningful on long-lived processes (gunicorn workers) with high-frequency identical-structure queries against PostgreSQL.

Compilation-only benchmark (reproducible via python benchmarks/run.py):

  • Simple filter: 113–123x faster compilation (37–39 μs saved)
  • Complex (JOINs + Q + annotate): 696–1,050x faster compilation (227–338 μs saved)

The variance in the complex scenario is hardware-dependent.

AST-based SerializerMethodField analyzer

Static analysis of DRF get_ methods using Python’s ast module — no Django imports, no runtime execution. Detects queryset calls (.filter(), .all(), .count(), .get(), .exists()) and list comprehensions iterating over querysets inside SerializerMethodField methods.

python manage.py check_serializers --app=myapp

Baseline regression detection

Save a snapshot of known issues, then fail CI when new query problems appear:

python manage.py check_queries --save-baseline=baseline.json
python manage.py check_queries --baseline=baseline.json --fail-on-regression

Comparison is by issue type + file, not line number — refactoring doesn’t break the baseline.

Other additions

  • Per-file/module analysis (–file, --module flags)
  • Smart prescription grouping (–group flag)
  • GitHub Actions PR annotations (::error file=…,line=…)
  • Async-safe: interceptor migrated from threading.local to contextvars.ContextVar
  • Benchmark dashboard (python manage.py query_doctor_report → Chart.js HTML)

Quality bar

  • 749 tests, 86% coverage
  • mypy strict, ruff clean
  • Full MkDocs documentation
  • Security audit: fingerprint never includes user-supplied values (tested with SQL injection strings — same fingerprint as normal values)

Links

GitHub: GitHub - hassanzaibhay/django-query-doctor: Automated diagnosis and prescriptions for slow Django ORM queries. Detects N+1, duplicates, missing indexes — and tells you exactly how to fix them. · GitHub
Docs: django-query-doctor

Happy to answer questions about the WhereNode walking approach for param extraction, the fingerprinting design decisions, or the async migration. The implementation has some interesting corners worth discussing if anyone is curious.

1 Like