drf-nplus
A DRF serializer-aware N+1 query detector for Django.
Unlike a generic query counter, drf-nplus attributes each SQL query to the
serializer field that triggered it and suggests the ORM fix:
[drf-nplus] GET /posts/ → 201 queries in 340.2ms | 2 repeated SQL templates (possible N+1)
PostSerializer.author: 100 queries ← possible N+1 — add .select_related("author")
PostSerializer.tags: 100 queries ← possible N+1 — add .prefetch_related("tags")
PostSerializer: 1 queries
Install
pip install drf-nplus
Or from source:
pip install -e /path/to/drf_nplus_project
Integrate
Add the middleware to your Django settings.py:
MIDDLEWARE = [
# ...
"drf_nplus.QueryCountMiddleware",
]
That's it. On every request the middleware logs a per-field query report and sets these response headers:
X-DRF-Queries— total query countX-DRF-Query-Time-Ms— wall time spent inget_responseX-DRF-NPlus-Fields— comma-joined serializer field paths flagged as N+1
Configure
All settings are optional. Defaults shown:
DRF_NPLUS = {
"ENABLED": True, # gate the middleware; typically set to DEBUG
"LOGGER": "drf_nplus", # standard `logging` logger name
"LOG_LEVEL": "WARNING", # level used to emit the report
"THRESHOLD": 2, # ≥ N identical queries from one field = N+1
"IGNORE_PATHS": (), # prefixes to skip, e.g. ("/admin/", "/static/")
"RESPONSE_HEADERS": True, # set the X-DRF-* headers
}
Wire the logger through your LOGGING config to route reports wherever you
already send logs:
LOGGING = {
"version": 1,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {
"drf_nplus": {"handlers": ["console"], "level": "WARNING"},
},
}
Use in tests
Installing drf-nplus auto-registers a pytest plugin. Two ways to guard tests:
Per-test marker:
import pytest
@pytest.mark.no_nplus
def test_post_list_is_efficient(db):
client.get("/posts/")
# Or with a custom threshold
@pytest.mark.no_nplus(threshold=5)
def test_moderate(db): ...
Suite-wide: pytest --nplus-strict (applies the guard to every test).
Manual context manager (for finer control):
from drf_nplus.testing import assert_no_nplus
from blog.serializers import PostSerializer
from blog.models import Post
def test_post_list_is_efficient(db):
qs = Post.objects.select_related("author").prefetch_related("tags")
with assert_no_nplus():
PostSerializer(qs, many=True).data
All three raise drf_nplus.NPlusOneDetected (an AssertionError subclass)
with the offending field paths, SQL, and suggested fix.
How it works
patches.install()wrapsrest_framework.serializers.Serializer.to_representationto push the current field name onto aContextVarstack for the duration ofget_attribute/to_representation.- The middleware attaches an
execute_wrapperto every configured database connection. When a query fires, the current stack path is snapshotted and attributed to that field. ContextVar(notthreading.local) so it behaves correctly under async views.
Example project
This repo ships a blog/ app that demonstrates the problem:
python manage.py migrate
python manage.py shell < seed.py
python manage.py runserver
# Unoptimized — ~201 queries
curl -s http://127.0.0.1:8000/posts/ > /dev/null
# Optimized — ~3 queries
curl -s http://127.0.0.1:8000/posts-optimized/ > /dev/null
Development
pip install -e ".[dev]"
pytest
Release files for drf-nplus 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| drf_nplus-0.1.0.tar.gz | 11.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| drf_nplus-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 23.0 kB
Release files / drf_nplus-0.1.0.tar.gz
| Download URL | drf_nplus-0.1.0.tar.gz |
|---|---|
| Size | 11.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6697e241b5c737ebfbbe4a12c4a8bc3c7e9ced4e7ad75e220eabbd6254cee806
|
|
BLAKE2b-256 checksum How to use checksums |
9c0f5a3e14e9d8d37e61592877602d975e140aec476b7d1327984ad6870e494c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / drf_nplus-0.1.0-py3-none-any.whl
| Download URL | drf_nplus-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6ced643ce0f0e31a8a55e2218e7c5a59555c61d8cffc37599508dfd6741c313b
|
|
BLAKE2b-256 checksum How to use checksums |
9b8567bddb659addf73363a3fad76e6f83441d00a0436dd55b2014a47558249b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|