In a Django REST Framework generic view, where do select_related() and prefetch_related() go so a nested serializer stops querying per row?
answer
- the serializer only reads
- the view owns the queryset
- a method called per request
- list and detail share it
basics
~20 sThey go on the view's queryset: the queryset class attribute or, usually, an overridden get_queryset(). DRF never optimises queries for you, and the serializer only reads attributes, so the view must load author with select_related() and tags with prefetch_related().
solid answer
~40 sIn the view, on the queryset the serializer will receive: the `queryset` class attribute or an overridden `get_queryset()`. A serializer only reads attributes such as `article.author` and `article.tags.all()`; DRF deliberately does not add `select_related()` or `prefetch_related()` itself — its docs call that too much magic. `ListModelMixin.list()` calls `get_queryset()`, runs the filter backends, paginates, then serializes the page, so a loading plan added there applies to every page and to `get_object()` on detail routes as well. Use `select_related()` for forward `ForeignKey`/`OneToOneField` relations like the author, and `prefetch_related()` for many-to-many and reverse relations like tags. Override `get_queryset()` when the plan depends on the request or action; a class attribute is safe too, because `GenericAPIView.get_queryset()` calls `.all()` on it for each request.
code
python · 16 linesfrom rest_framework import viewsets
from blog.models import Article
from blog.serializers import ArticleSerializer
class ArticleViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ArticleSerializer
def get_queryset(self):
return (
Article.objects
.select_related("author") # nested AuthorSerializer
.prefetch_related("tags") # tags = TagSerializer(many=True)
.order_by("-published_at")
)go deeper
Remember the rule: the view's get_queryset() loads relations, the serializer only reads them. select_related for foreign keys, prefetch_related for many-to-many and reverse relations.
Walk through list(): get_queryset, filter_queryset, paginate_queryset, then the serializer. Explain why the pk-only shortcut spares foreign keys but not to-many pk lists.
Keep the loading plan next to the serializer it serves, per action if needed, and guard it with a query-count test so a new field cannot silently undo it.
Decide whether each team hand-maintains loading plans or relies on a framework-level safety net, and how regressions are caught before production.
## The problem in one sentence A Django REST Framework (DRF) **serializer** turns each model instance into a dict by reading attributes. When a field reads a relation — a nested `AuthorSerializer` reads `article.author`, a `tags` field reads `article.tags.all()` — Django's ORM loads it **on first access, per instance**. Serializing 50 articles therefore costs one query for the page plus one or more per article, per relation. The fix is to load those relations up front, and the place to do it is the **view's queryset**, not the serializer. ## Why the view, not the serializer - The serializer receives an already-built queryset (or a list — the page) and iterates it; by then, the loading plan is fixed. - DRF does not inspect your serializer and optimise the queryset. The relations guide says so directly: it does not automatically optimise querysets with `select_related` and `prefetch_related` because that would be *too much magic*, and it is the programmer's responsibility. - The generic views' documentation puts it in one line: optimise the queryset in `get_queryset()` or on the `queryset` class attribute. ## How the queryset flows through a list request `ListModelMixin.list()` — used by `ListAPIView` and `ModelViewSet` — does this: 1. `queryset = self.filter_queryset(self.get_queryset())` — your loading plan is applied here, then filter backends add their `filter()` calls. 2. `page = self.paginate_queryset(queryset)` — the pagination class slices the queryset, which evaluates it for just that page. 3. `serializer = self.get_serializer(page, many=True)` — the serializer reads the already-loaded rows. Because `select_related()` and `prefetch_related()` are lazy QuerySet methods, adding them in step 1 costs nothing until step 2 evaluates the page. The prefetch then runs for the page's rows only. Detail routes use the same method: `GenericAPIView.get_object()` starts from `self.filter_queryset(self.get_queryset())`, so a single article is loaded with its author joined in as well. ## Which method for which relation | Serializer field reads | Relation kind | Load with | |---|---|---| | `article.author` (nested or dotted `source=`) | forward `ForeignKey` / `OneToOneField` | `select_related("author")` — one SQL join | | `article.author.team` | chained forward relations | `select_related("author__team")` | | `article.tags.all()` | `ManyToManyField` | `prefetch_related("tags")` — one extra query for all rows | | `article.comments.all()` | reverse `ForeignKey` | `prefetch_related("comments")` | Two DRF-specific details decide whether a field needs any of this: - A single `PrimaryKeyRelatedField` (the `ModelSerializer` default for a foreign key) uses a **pk-only shortcut**: it reads the stored `author_id` and never loads the author, so it needs no `select_related()`. - A to-many field *always* calls `.all()` on the related manager — even a list of pks from `PrimaryKeyRelatedField(many=True)` — so tags cost one query per article unless prefetched. ## `queryset` attribute or `get_queryset()` - A class attribute such as `queryset = Article.objects.select_related("author")` is fine: `GenericAPIView.get_queryset()` calls `.all()` on it for every request, so results are not cached between requests. Code that reads `self.queryset` directly would bypass that. - Override `get_queryset()` when the plan depends on the request: a different serializer per action, user-scoped rows, or an expensive prefetch that only the list needs. ## Common mistakes - **Optimising in the serializer.** Calling `prefetch_related_objects()` or running queries in `to_representation()` happens per object or per page after the rows are loaded; the queryset is the place. - **Losing the plan in an override.** A custom `get_queryset()` that returns `Article.objects.filter(...)` without the `select_related()` the old class attribute had silently brings the per-row queries back. - **Filtering after prefetching.** A filter backend that adds `.filter()` is fine — it narrows the articles — but code that later calls `article.tags.filter(...)` asks a new question the prefetch cache cannot answer. - **Prefetching what is never rendered.** Each `prefetch_related()` lookup is one more query per request, so drop lookups whose fields left the serializer. ## What this does not fix Loading relations does not help a `SerializerMethodField` whose method runs a *new* query (a `filter()` or `count()` on a relation), nor the Python cost of serializing thousands of rows. Those are separate problems with separate fixes.
- Does a ModelSerializer's default field for article.author need select_related('author')?No. The default `PrimaryKeyRelatedField` renders the stored `author_id` through DRF's pk-only shortcut and never loads the author row. You need `select_related()` once a field reads the author object itself: a nested serializer, a dotted `source='author.name'`, or a `StringRelatedField`.
- Why is it safe to put select_related() on the queryset class attribute of a DRF view?`GenericAPIView.get_queryset()` calls `.all()` on a `QuerySet` class attribute, which clones it, so each request evaluates a fresh query and results are never shared between requests. Reading `self.queryset` directly in your own code would skip that clone.
saying these in an interview costs you the question
- DRF adds select_related() automatically for nested serializers.
- The select_related() call belongs in the serializer's Meta class.
- A list of tag pks costs nothing extra because only ids are rendered.
- Putting prefetch_related() on the queryset class attribute caches results across requests.
- Optimising get_queryset() only helps the list route, not retrieve.