Skip to main content

drf-prefetch-hint

ci pypi

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:

  1. Run it against the viewset, from your project root:

    python manage.py prefetch_hint shop.views.AuthorViewSet --count 25
    
  2. Paste the expression into that viewset's get_queryset().

  3. Run it again. The query count should drop and the fields should disappear from the report. Anything still listed is either a SerializerMethodField or 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:

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.

  • SerializerMethodField cannot 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=False without --force.
  • --count matters. 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

drf_prefetch_hint-0.1.0.tar.gz (29.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

drf_prefetch_hint-0.1.0-py3-none-any.whl (27.2 kB view details)

Uploaded Python 3

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

Hashes for drf_prefetch_hint-0.1.0.tar.gz
Algorithm Hash digest
SHA256 98da04df6cf37eea49a17629f303fffa335674b093b044bfa47a1e08b3eac338
MD5 fc4d57b3401b9e2ec00a245eb94ec067
BLAKE2b-256 af79fa7a9f37c64061412f8ae4eacaf7a7d354421f7def3b3626980b1d8ea471

See more details on using hashes here.

File details

Details for the file drf_prefetch_hint-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for drf_prefetch_hint-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9ba1245c991f969068e31ff583c3e29f24f1a1fb3c01677dbb4775c4883124fc
MD5 9c59cc4722ae2854dc9daff97ddc69bb
BLAKE2b-256 b056f0f3e22de7ac436ecb1b41fc51691fe0332733b6a107327e03beddf058f4

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page