← Back to portfolio

Open source · Python package

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.

pip install drf-prefetch-hint
v0.1.0 · beta PyPI GitHub MIT Python 3.10–3.13

Django 4.2 / 5.0 / 5.1 · DRF 3.14+ · dev-only, no middleware, no runtime hook.

The demo

One command. The whole answer.

Point it at a viewset. It reports every relation your serializer touches, what kind it is, how many queries it cost, and where in your code it came from — then hands you the ORM expression.

Terminal session running the prefetch_hint management command against AuthorViewSet. The command reports 51 queries on 10 objects, lists four serializer fields with their relation kind and per-field query cost, prints the select_related and prefetch_related calls to add to get_queryset, projects a drop to about 13 queries, and flags the summary field as not auto-fixable.
zsh
$ 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

Real, selectable text — not a screenshot. Scroll the panel sideways on narrow screens.

The result

51 → 3queries

pasting the suggestion verbatim

Projected ~13; the real result is usually better than projected, because a prefetch often satisfies method-field queries for free.

The change

Before and after

Before 51 queries
class AuthorViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = AuthorSerializer

    def get_queryset(self):
        return Author.objects.all()
After 3 queries
from django.db.models import 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 can't add — Prefetch, and any model used inside Prefetch(queryset=…).

The hard part

Nesting, not flattening

reviews hangs off books, which is itself a reverse FK — so it belongs on the inner Prefetch queryset, not flattened onto the outer one. Working that out by hand is the hour you don't spend.

Internals

How it works

  1. 01

    Wrap DRF’s get_attribute and to_representation; keep a ContextVar stack of the field currently resolving.

  2. 02

    Django’s connection.execute_wrapper sees every query fire and tags it with whatever is on top of that stack — every query attributed to the exact serializer field that caused it.

  3. 03

    Walk those field paths through model._meta to decide JOIN vs. second query.

  4. 04

    Assemble the ORM expression and ast.parse-validate it before printing.

Everything runs inside a transaction that is always rolled back; DRF is left unpatched on every exit path including exceptions.

Usage

Flags

--count default 25
How many objects to build the sample queryset with.
--user default AnonymousUser
The user the profiled request runs as.
--action default list
The viewset action to profile.
--raw
Raw output, without the formatted report.
--no-color
Disable ANSI colour in the output.
--force
Run anyway when DEBUG is False. Refuses without it.

Honest limits

Limitations

Method fields can't be resolved

SerializerMethodField runs arbitrary code. It's reported and marked manual fix needed — the tool won't guess, because a wrong guess is worse than silence.

The projection is an estimate

Not a promise. The printed number is what the analysis expects, not a measured result.

One code path only

Suggestions reflect the one code path that ran, with the user and action you passed. A different --user or --action can produce a different answer.

Development only

It refuses to run under DEBUG=False without --force.

Scope

What it does not do

It's a one-shot answer machine, not a monitor. These tools cover the jobs it deliberately leaves alone.

DRF serializers only — not plain class-based views, the Django admin, GraphQL, or Django Ninja.

Try it

One command away.

pip install drf-prefetch-hint

Bug reports with a minimal reproducing serializer are the most useful thing to send.