drf-prefetch-hint
Tells you exactly which select_related / prefetch_related to add to your DRF viewset.
Other tools tell you that you have an N+1. This one writes the fix.
$ python manage.py prefetch_hint shop.views.AuthorViewSet --count 10
AuthorViewSet — 51 queries on 10 objects
books.reviews reverse FK → prefetch_related
20 queries shop/views.py:7
books reverse FK → prefetch_related
10 queries shop/views.py:11
publisher.name FK → select_related
10 queries shop/views.py:10
summary method → 10 queries, manual fix needed
10 queries shop/views.py:12
Add to get_queryset():
.select_related("publisher")
.prefetch_related(Prefetch("books", queryset=Book.objects.prefetch_related("reviews")))
Projected: 51 → ~13 queries (estimate)
Not auto-fixable:
summary — SerializerMethodField runs arbitrary code
Pasting that suggestion verbatim takes this endpoint from 51 queries to 3.
Note what it worked out on its own: reviews hangs off books, which is itself a
reverse FK, so it belongs in a prefetch_related on the inner Prefetch
queryset — not flattened onto the outer one. That is the part that costs you an hour.
Requirements
| Supported | |
|---|---|
| Python | 3.10 – 3.13 |
| Django | 4.2, 5.0, 5.1 |
| Django REST Framework | 3.14+ |
Tested in CI against the oldest and newest supported combinations.
Install
pip install drf-prefetch-hint
INSTALLED_APPS = [
...
"prefetch_hint",
]
It is a development tool. There is no middleware and no runtime hook — nothing runs unless you type the command. You can leave it out of production requirements entirely.
When to use it
You have one list endpoint that is slow. You already know it is an N+1. You do not
want to spend an hour working out the exact nested Prefetch incantation.
The loop is:
-
Run it against the viewset, from your project root:
python manage.py prefetch_hint shop.views.AuthorViewSet --count 25
-
Paste the expression into that viewset's
get_queryset(). -
Run it again. The query count should drop and the fields should disappear from the report. Anything still listed is either a
SerializerMethodFieldor something worth a closer look.
It is not a monitor and not a linter. Point it at one endpoint when that endpoint is the problem.
Applying the fix
Before:
class AuthorViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = AuthorSerializer
def get_queryset(self):
return Author.objects.all()
After — the generated expression pasted onto the end of the chain:
from django.db.models import Prefetch # only needed when the output uses Prefetch(...)
from shop.models import Author, Book
class AuthorViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = AuthorSerializer
def get_queryset(self):
return (
Author.objects.all()
.select_related("publisher")
.prefetch_related(
Prefetch("books", queryset=Book.objects.prefetch_related("reviews"))
)
)
You supply two imports the tool cannot add for you: Prefetch, and whichever
models appear inside a Prefetch(queryset=...) (here, Book). If the output is
only strings, you need neither.
Usage
python manage.py prefetch_hint <dotted.path.to.ViewSet> [options]
| Flag | Default | Purpose |
|---|---|---|
--count N |
25 | Objects to serialize. Must be > 1 or N+1 is invisible. |
--user <pk|username> |
AnonymousUser |
For permission-gated get_queryset() |
--action <name> |
list |
ViewSet action — affects serializer selection |
--raw |
off | Print raw SQL per field group |
--no-color |
off | Plain output for piping |
--force |
off | Run even when DEBUG=False |
Everything runs inside a transaction that is always rolled back. The command never writes, and DRF is left unpatched on every exit path including exceptions.
Troubleshooting
get_queryset() raised AttributeError — your get_queryset() depends on the
request user. Pass one:
python manage.py prefetch_hint shop.views.AuthorViewSet --user 1
python manage.py prefetch_hint shop.views.AuthorViewSet --user alice
It analysed the wrong serializer. Most real viewsets return a different serializer per action. Pass the one you care about:
python manage.py prefetch_hint shop.views.AuthorViewSet --action retrieve
DEBUG=False. prefetch_hint is a development tool — intentional. This
serializes real rows; it is not meant for production. --force overrides it if
you know what you are doing.
It reported nothing. Either the viewset is already optimized (good — it is
built to stay silent in that case) or --count is too low for the pattern to show.
Try --count 50.
A field is listed but no fix was generated. It is a SerializerMethodField,
or a path that could not be resolved through _meta. See Limitations.
What it does not do
Deliberately. These are other packages' jobs and several already do them well:
- Watching queries as you browse — use django-debug-toolbar
- Failing CI when a view gets slower — use django-query-guard or django-perf-rec
- Detecting N+1 at runtime across your whole app — use zealot
- Production monitoring — use Sentry or Scout
No middleware, no pytest plugin, no CI mode, no config file, no web UI, no auto-patching of your source. One command, one output.
It also only supports DRF serializers — not plain Django views, generic CBVs, templates, the admin, GraphQL, or Django Ninja.
Limitations
Read these before trusting the output.
SerializerMethodFieldcannot be resolved. It runs arbitrary code, so nothing in the field declaration reveals which relations it touches. These are reported with their query count and marked manual fix needed. The tool will not guess — a wrong guess is worse than silence.- The projection is an estimate, not a promise. It assumes every method-field
query survives the fix. In practice a prefetch often satisfies them for free, so
the real result is frequently better than projected. It also does not model
queries fired outside serialization, such as a paginator's
COUNT. - Suggestions are a starting point, not gospel. They reflect the one code path that ran, with the user and action you passed.
- Development only. It refuses to run under
DEBUG=Falsewithout--force. --countmatters. Too small and a relation may not look like an N+1 yet.
How it works
DRF resolves each serializer field through get_attribute and to_representation;
the package wraps both and keeps a ContextVar stack of whichever field is
currently being resolved. Django's connection.execute_wrapper sees every query as
it fires and tags it with whatever is on top of that stack — so each query is
attributed to the exact serializer field that caused it. Those field paths are then
walked through model._meta to decide whether each one needs a JOIN or a second
query, and the result is assembled into an ORM expression that is
ast.parse-validated before it is ever printed.
More detail, including the two traps that make this harder than it looks, in docs/HOW_IT_WORKS.md.
Contributing
See CONTRIBUTING.md. Bug reports with a minimal serializer that reproduces the problem are the most useful thing you can send.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file drf_prefetch_hint-0.1.0.tar.gz.
File metadata
- Download URL: drf_prefetch_hint-0.1.0.tar.gz
- Upload date:
- Size: 29.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
98da04df6cf37eea49a17629f303fffa335674b093b044bfa47a1e08b3eac338
|
|
| MD5 |
fc4d57b3401b9e2ec00a245eb94ec067
|
|
| BLAKE2b-256 |
af79fa7a9f37c64061412f8ae4eacaf7a7d354421f7def3b3626980b1d8ea471
|
File details
Details for the file drf_prefetch_hint-0.1.0-py3-none-any.whl.
File metadata
- Download URL: drf_prefetch_hint-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9ba1245c991f969068e31ff583c3e29f24f1a1fb3c01677dbb4775c4883124fc
|
|
| MD5 |
9c59cc4722ae2854dc9daff97ddc69bb
|
|
| BLAKE2b-256 |
b056f0f3e22de7ac436ecb1b41fc51691fe0332733b6a107327e03beddf058f4
|