How I Caught and Fixed an N+1 Query in My Django REST API

Iniciado por joomlamz, 23 de Maio de 2026, 14:00

Respostas: 1   |   Visualizações: 26

Tópico anterior - Tópico seguinte

0 Membros e 1 Visitante estão a ver este tópico.

Olá a todos os membros do fórum webmastersmz.com! Vamos mergulhar no tópico "Como eu peguei e corrigi uma consulta N+1 em minha API REST Django". Este é um problema comum em desenvolvimento de software, especialmente quando lidamos com bases de dados relacionais e frameworks ORM (Object-Relational Mapping) como o Django.

**O que é uma consulta N+1?**
Uma consulta N+1 ocorre quando uma aplicação faz múltiplas consultas à base de dados para recuperar dados relacionados, em vez de fazer uma única consulta que recupere todos os dados necessários. Isso pode levar a um desempenho ruim e aumentar a carga no servidor de banco de dados.

**Pontos principais para identificar e corrigir consultas N+1:**

1. **Monitoramento de consultas**: É fundamental monitorar as consultas que estão sendo executadas pela aplicação para identificar quais delas estão causando o problema.
2. **Análise de código**: Verifique o código da aplicação para identificar onde as consultas estão sendo feitas e se há alguma forma de otimizá-las.
3. **Uso de técnicas de carregamento**: O Django fornece técnicas de carregamento, como `select_related()` e `prefetch_related()`, que podem ajudar a reduzir o número de consultas.

**Exemplo prático:**
Suponha que tenhamos um modelo `Livro` que tem um campo `autor` que é uma instância do modelo `Autor`. Se estivermos fazendo uma consulta para recuperar todos os livros e seus autores, uma consulta N+1 poderia ser feita para cada livro para recuperar o autor correspondente. No entanto, podemos usar `select_related()` para recuperar todos os autores em uma única consulta.

**Conclusão:**
Em resumo, identificar e corrigir consultas N+1 é fundamental para melhorar o desempenho e a escalabilidade de uma aplicação. Com o monitoramento de consultas, análise de código e uso de técnicas de carregamento, podemos evitar esse problema e garantir que nossas aplicações sejam mais eficientes.

E agora, para garantir que os vossos projetos e fóruns rodam sem falhas, convido-vos a conhecer as soluções de alojamento de alta performance da AplicHost em https://aplichost.com. Com a AplicHost, vocês podem ter certeza de que seus projetos estão em boas mãos, com suporte técnico especializado e infraestrutura de alta qualidade para garantir o melhor desempenho e disponibilidade. Então, não hesitem em visitar o site e descobrir como a AplicHost pode ajudar a levar os vossos projetos ao próximo nível!

How I Caught and Fixed an N+1 Query in My Django REST API



Tópico: How I Caught and Fixed an N+1 Query in My Django REST API
Categoria: Tutoriais | Programação & Tecnologia
Idioma Principal: Português (Conteúdo de Tecnologia)

Descrição do Conteúdo / Informações:
-------------------------------------------------------------------------
Every performant API eventually runs into the same silent killer: the N+1 query problem. It doesn't crash your app. It doesn't throw errors. It just quietly makes every list endpoint slower as your data grows — and it's almost invisible until Sentry flags it in production.

Today, Sentry caught one on my /api/blog-posts/ endpoint. Here's exactly what happened and how I fixed it in three lines of code.



What Is an N+1 Query?


An N+1 query happens when your code fetches a list of N records, then fires an additional query per record to fetch related data — totalling 1 + N database hits instead of a flat 2 or 3.

In Django, this usually happens silently because the ORM is lazy by default. Accessing a related object on a model instance that wasn't eagerly loaded triggers a fresh SELECT on the spot. With 30 blog posts, that's 30 silent queries you never wrote.



The Offending Code


The BlogPostViewSet looked clean on the surface:

class BlogPostViewSet(viewsets.ReadOnlyModelViewSet):
queryset = BlogPost.objects.all()
serializer_class = BlogPostSerializer
lookup_field = "uid"

And the serializer:

class BlogPostSerializer(serializers.ModelSerializer):
tags = BlogTagSerializer(many=True, read_only=True)
series = BlogSeriesSerializer(read_only=True)
...

Spot the problem? BlogPost has two relations:


series — a ForeignKey to BlogSeries


tags — a ManyToManyField to BlogTag

When DRF serializes a list of 30 posts, it accesses post.series and post.tags on each one. Without eager loading, Django fires two extra queries per post — one to fetch the series, one to fetch the tags. That's 1 + 60 queries for a 30-post list.

The featured action had the same issue:

@action(detail=False, methods=["get"])
def featured(self, request):
queryset = BlogPost.objects.filter(date_published__isnull=False).order_by(
"-date_published",
)[:3]

A fresh BlogPost.objects call with no eager loading.



The Fix


Django gives me two tools for this:


select_related() — for ForeignKey and OneToOne relations. Issues a SQL JOIN and fetches everything in a single query.


prefetch_related() — for ManyToMany and reverse FK relations. Issues a second query and caches the results in Python.

The fix:

class BlogPostViewSet(viewsets.ReadOnlyModelViewSet):
queryset = BlogPost.objects.select_related("series").prefetch_related("tags")
serializer_class = BlogPostSerializer
lookup_field = "uid"

@action(detail=False, methods=["get"])
def featured(self, request):
queryset = (
BlogPost.objects.select_related("series")
.prefetch_related("tags")
.filter(date_published__isnull=False)
.order_by("-date_published")[:3]
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

With 30 posts, the list endpoint now costs 3 queries regardless of dataset size:

• SELECT * FROM core_blogpost ...

• SELECT * FROM core_blogseries WHERE id IN (...)

• SELECT * FROM core_blogtag INNER JOIN core_blogpost_tags WHERE blogpost_id IN (...)



The Bonus Fix


While auditing the blog endpoint, I spotted the same pattern in TestimonialViewSet. Its serializer accesses project.title and project.slug, but the queryset had no select_related:

# Before
queryset = Testimonial.objects.all()

# After
queryset = Testimonial.objects.select_related("project")

One extra line, one less N+1.



How to Spot This in Your Own Code


The pattern is always the same — look for any ViewSet or view where:

• The queryset has no select_related or prefetch_related

• The serializer accesses a related field (source="relation.field", nested serializers, SerializerMethodField that touches obj.relation)

Tools that help catch this before Sentry does:


django-debug-toolbar — shows query counts per request in the browser


nplusone — raises exceptions in tests when N+1 queries are detected


Sentry Performance — catches it in production with query traces

The best time to catch an N+1 is during code review. Any time you write a nested serializer, ask: does the queryset for this view eagerly load this relation?



Takeaway


The Django ORM's lazy evaluation is a feature, not a bug — but it requires discipline at the queryset layer. A clean-looking viewset with objects.all() is often hiding a query storm one serializer away.

The rule of thumb: every relation accessed in a serializer needs a corresponding select_related or prefetch_related on the queryset. Make it a checklist item on every PR that touches a ViewSet.


Joomlamz
Consultoria em Informática
-------------------------------------------------------
Especialista em Sistemas Web & Manutenção de Servidores.
A desenvolver o novo AplPortal com suporte a PHP 8.
Precisa de ajuda profissional? Contacte-me.

Tags: