Controlling Nested Serializer Depth and Query Performance in Django REST
When ModelSerializer nests related models, query performance can silently degrade. This post shows how to use depth, select_related, and prefetch_related to keep APIs fast and secure.
09 Aug 2025, 19:39 UTC

The N+1 Quiet Spot in Nested ModelSerializers
When you use ModelSerializer to expose a model with a foreign key, the API response can trigger extra database queries per item. This is the N+1 problem, and it hides in plain sight when you add a nested serializer.
The depth Attribute
DRF's depth field on a ModelSerializer automatically expands related fields into the response. Setting depth = 1 on our OrderSerializer will include the related Customer's fields without writing a custom nested serializer.
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
depth = 1
fields = ['id', 'total', 'customer']
Under the hood, DRF uses depth to decide how many levels of related objects to serialize. However, depth alone does not reduce query count — it can actually increase them if the view does not optimize the initial query.
Optimizing Queries with select_related and prefetch_related
To prevent the N+1 pattern, the viewset must fetch related objects in the initial query. For foreign keys, select_related performs a SQL join, fetching related data in a single statement.
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.select_related('customer').all()
serializer_class = OrderSerializer
With this combination, fetching 100 orders triggers exactly one database query, regardless of depth.
Worked Example: Order and Customer
Consider these models:
class Customer(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField()
class Order(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
order_date = models.DateTimeField()
total = models.DecimalField(max_digits=10, decimal_places=2)
Serializer with depth = 1 and viewset with select_related:
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
depth = 1
fields = ['id', 'total', 'customer']
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.select_related('customer').all()
serializer_class = OrderSerializer
An API call returning 100 orders will issue one SELECT with a join on the customer table, rather than 101 queries.
Trade‑off: depth Versus Explicit Nesting
- depth convenience: Less boilerplate, faster to write, useful for admin or internal APIs.
- explicit nesting: Gives full control over which fields appear, enables custom validation, and makes query optimization intentions clear.
- performance risk: Using
depthwithoutselect_related/prefetch_relatedcan silently increase query counts, especially beyond depth 1.
For production APIs, explicit serializer fields paired with optimized querysets are generally safer.
Verification Checklist
- Add
depth = 1(or 0) to your ModelSerializer Meta and observe the generated SQL. - Annotate your viewset queryset with
select_related()for foreign keys orprefetch_related()for many-to-many. - Use Django Debug Toolbar's
SQLpanel to confirm query count matches expectations. - If query count is higher than expected, replace
depthwith explicit nested serializers and adjustfields.
By matching depth with optimized querysets, you keep your API responsive without sacrificing the readability that nested serializers provide.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.