Static N+1 query detection for Django (LSP-based)
django-check is a static analyzer and Language Server that detects N+1 query patterns in Django code before runtime. It inspects queryset construction and related-field access to warn when relations are accessed without proper prefetching (select_related, prefetch_related).
It works inside the editor or directly from CLI.
Warning
This project is in active development and not yet production-ready.
- APIs are unstable
- Diagnostics may be incomplete or incorrect
- Expect breaking changes without notice
Django’s ORM makes it easy to accidentally introduce N+1 queries that:
- pass tests,
- look correct in code review,
- only show up under load.
Runtime tools (django-silk, nplusone) are focuesd in runtime optimiezation. django-check make static analysis before the runtime.
- Compute the graph of the
Models in the app - Zero runtime overhead
- LSP-based diagnostics at edit time
- No code instrumentation required
- Works with any editor that supports LSP
Not available yet, should be compiled from source.
Usage: djch
Commands:
server Start as a Language Server (normally handled by the IDE)
check Analyze the current directory tree for N+1 queries
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help (see more with '--help')
-V, --version Print version
Check from CLI using djch check. You will get an output like this:
app/foo/bar/views/tier1.py:210:22: [N+1] rp.ticker_benchmark
Potential N+1 query: accessing `rp.ticker_benchmark` inside loop
app/apps/crawler/tasks.py:48:17: [N+1] ticker.industry
Potential N+1 query: accessing `ticker.industry` inside loop
app/apps/crawler/views.py:62:20: [N+1] stream.streamer
Potential N+1 query: accessing `stream.streamer` inside loop
app/apps/foo/selectors/anointed.py:43:19: [N+1] anointed.pattern
Potential N+1 query: accessing `anointed.pattern` inside loop
Neovim 0.11 ships with a stable built-in LSP client.
Minimal setup:
-- lua/init.lua
vim.lsp.enable("djch")
-- lsp/djch.lua
return {
cmd = { "djch", "server" },
filetypes = { "python" },
root_markers = { 'manage.py', 'pyproject.toml', '.git' }
}
This registers django-check as a first-class LSP server.
If you are already attaching multiple LSPs to Python buffers (e.g. Pyright), Neovim will merge diagnostics correctly.
You can install the VSCode extension from, or directly in VSCode Extensions Market Place.
https://marketplace.visualstudio.com/items?itemName=richardhapb.Django-Check
# N+1 query detected
users = User.objects.all()
profiles = [user.profile in user for users] # N+1
Explanation:
usersis evaluated onceuser.profiletriggers one query per iteration
users = User.objects.select_related("profile").all()
profiles = [user.profile in user for users]
users = User.objects.all()
for user in users:
user.profile.bio # N+1
Explanation:
usersis evaluated onceuser.profiletriggers one query per iteration
users = User.objects.select_related("profile").all()
for user in users:
user.profile.bio
django-check will clear the diagnostic once the relation is prefetched.
- Parse Python source into an AST
- Identify Django model classes and relationships
- Track QuerySet-producing expressions
- Track iteration boundaries
- Detect attribute access that implies ORM resolution
- Verify whether the required relation is prefetched
- Zero config
- Just works out of the box
- Editor feedback must be actionable, not noisy
- Interprocedural analysis requires type hints on QuerySet parameters
- Limited understanding of:
annotate,aggregate- complex custom managers
- Custom queryset method summaries
- Templates integration
This project lives at the intersection of:
- Python AST
- Django ORM semantics
- LSP protocol design
If you are interested in any of those, contributions are welcome.
Documentation contributions are welcome, the goal is make this tool easy to use.
MIT
