> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Reviewing the N+1 Query Hidden Inside a Serializer
- URL: https://debugly.dev/reviewing-n-plus-one-in-serializer/
- Published: 2026-10-09T15:34:00.000Z
- Updated: 2026-10-10T13:59:19.000Z
- Description: The list endpoint issued one query for the rows and one per row for the relation, and the extra queries were invisible in the view because the serializer…
- Author: Rohit Bhadani
- Tags: Code Review, Performance, Django

Between the four seconds and the review's miss sit 6 quiet assumptions, and the incident is the story of one of them failing.

Here is the shape of the N+1 that lives outside the view, and the one property that makes it invisible: the view's queryset is correct, and the extra queries are issued by the serialization layer, so reading the view shows one query and the log shows four hundred.

This was Django REST Framework, and the mechanics are the same for any framework where the serializer resolves relations lazily.

## What the review saw

The review saw the view, and the view was the queryset, and the queryset was the filter, and the filter was the one query, and the one query was the correct, and the correct was the pass, because the view did the right thing, and the right thing was the select, and the select was the rows, and the rows were the response's input.

The review did not see the serializer, and the serializer was the field, and the field was the relation, and the relation was the lazy, and the lazy was the query on access, and the access was the per row, and the per row was the four hundred, and the four hundred was the N plus one, and the N plus one was the invisible, because the serializer is not the view, and the review reads the view.

The invisible is the layering, because the query is issued where the data is touched, and the touched is the serialization, and the serialization is the response's construction, and the construction is the per row, and the per row is the multiplication, and the multiplication is the four seconds, and the four seconds is the review's question that nobody asked, which is how many queries does this endpoint actually run.

## Why the count was the diagnosis

The diagnosis was the query log, and the log was the four hundred, and the four hundred was the count, and the count was the N plus one, and the N plus one was the pattern, and the pattern was the one for the list and the N for the relations, and the relations were the author and the category and the tags, and the three were the three N, and the three N was the four hundred, and the four hundred was the fix's target.

The count is the diagnosis, because the count is the truth, and the truth is the log, and the log is the database's voice, and the voice is the number, and the number is the argument, and the argument is the fix, and the fix is the prefetch, and the prefetch is the three queries instead of the four hundred, and the three was the forty milliseconds.

This is the same discipline as [the pg\_stat\_statements ledger that showed the slowest queries](https://debugly.dev/pg-stat-statements-ledger/), and the shared property is the one worth fixing.

## The questions that catch it

**How many queries does this endpoint run?** The answer is the count, and the count should be the constant, and the constant is the fix, and the fix is the prefetch, and the question is the review's first, because the count that grows with the rows is the N plus one.

**Where is the data touched?** The answer is the layer, and the layer is the serializer, and the serializer is the lazy access, and the access is the query, and the query is the multiplication, and the multiplication is the fix's target, and the question is the review's second, because the view is not where the queries are issued.

**Is the relation prefetched or selected?** The answer is the strategy, and the strategy should be the explicit, and the explicit is the prefetch\_related or the select\_related, and the two are the fix, and the fix is the declaration, and the declaration is the review's third.

**What happens at a hundred rows versus ten?** The answer is the scaling, and the scaling is the linear growth, and the growth is the N plus one, and the N plus one is the fix, and the question is the review's fourth, because the test with ten rows hides the pattern that a hundred reveals.

## The fix

**Added the prefetch to the queryset.** The view's queryset gained the prefetch\_related for the three relations, and the prefetch was the three queries, and the three was the constant, and the constant was the fix, and the fix was the four seconds becoming the forty milliseconds.

**Made the serializer's expectations explicit.** The serializer declares the relations it needs, and the declares is the contract, and the contract is the view's obligation, and the obligation is the prefetch, and the prefetch is the fix, and the fix is the coupling made visible, because the serializer's needs should be readable from the serializer.

**Added a query count assertion in the test.** The test asserts the number of queries for the list endpoint, and the assert is the guard, and the guard is the regression's catch, and the catch is the prevention, and the prevention is the test, and the test is the fix, because the count that grows is the N plus one returning.

**Logged the query count in development.** The middleware prints the count per request, and the printed is the visible, and the visible is the review's input, and the input is the catch, and the catch is the prevention, and the prevention is the tooling, and the tooling is the discipline.

## What I now do

An N+1 can live in the serializer rather than the view, so reading the view shows one query while the endpoint runs one per row per relation. Ask how many queries the endpoint runs, where the data is touched, and whether the relations are prefetched, then assert the query count in a test.

The endpoint took four seconds and the view ran one query, because the serializer resolved three relations per row and each resolution was its own query. I now check for that property first, because it is where the failure actually lives.