O "Assassino Silencioso" de Performance: O Problema N+1 (e como resolvê-lo no REST e GraphQL)
Se você já viu uma aplicação funcionar perfeitamente em localhost com 10 registros, mas se arrastar em produção com 10.000, há uma grande chance de você ter sido vítima do Problema N+1.
Recentemente, mergulhei em diversas discussões sobre arquitetura de APIs e otimização de banco de dados. O que encontrei foi um consenso: o N+1 não é apenas um "glitch" técnico, é um erro arquitetural fundamental que afeta tanto REST quanto GraphQL, mas de formas diferentes.
O que é exatamente?
O cenário clássico: Você quer listar posts de um blog e o autor de cada post.
- A Query Inicial (1): Você busca os posts (
SELECT * FROM posts). Retorna 100 registros. - O Loop (N): Para cada um dos 100 posts, seu código (ou ORM) faz uma nova query para buscar o autor (
SELECT * FROM authors WHERE id = ?).
Resultado: 101 queries para uma tarefa que deveria levar 1 ou 2. Em um ambiente de nuvem, onde latência e I/O custam dinheiro, isso é queimar orçamento à toa.
N+1 no REST vs. GraphQL
Embora o problema seja o mesmo, a manifestação e a solução mudam drasticamente dependendo da sua arquitetura.
No REST (e ORMs clássicos)
No mundo REST, o problema geralmente nasce do uso ingênuo de ORMs (como Hibernate, TypeORM ou Prisma) e Lazy Loading.
- Cenário: Você tem um endpoint
GET /posts. Seu serializer JSON itera sobre os objetos e, ao tocar na propriedadepost.author, o ORM dispara uma query silenciosa. - A Solução Clássica (Eager Loading): A maioria dos ORMs resolve isso com um simples
.include()ouJOIN FETCH.- Exemplo (SQL mental): Em vez de N queries, você faz um
JOINe traz tudo de uma vez. - Trade-off: Você pode acabar trazendo dados demais (Over-fetching) se não cuidar dos campos selecionados.
- Exemplo (SQL mental): Em vez de N queries, você faz um
No GraphQL: O Buraco é Mais Embaixo
O GraphQL foi criado pelo Facebook em 2012 justamente para resolver o over-fetching e o under-fetching do REST em redes móveis ruins. Ironicamente, ele tornou o problema N+1 mais difícil de detectar no backend.
Como cada campo no GraphQL é resolvido por uma função independente (resolver), o servidor não sabe naturalmente que você vai pedir os autores de uma lista de posts até que ele comece a executar a query.
- Se você pede
posts { author { name } }, o resolver depostsroda uma vez, mas o resolver deauthorroda N vezes.
Como Resolver de Verdade?
Não basta "fazer funcionar", precisa escalar. Aqui estão as estratégias modernas baseadas na arquitetura que você usa:
1. O Padrão DataLoader (A Bala de Prata do GraphQL)
Se você usa GraphQL (Node, Java, Python), o DataLoader é obrigatório.
Ele age como um "pedágio" que retém as requisições dos resolvers. Em vez de ir ao banco imediatamente, ele espera um "tick" do event loop, agrupa todos os IDs de autores solicitados e faz uma única query:
SELECT * FROM authors WHERE id IN (1, 2, 5, ...)
Depois, ele distribui os resultados de volta para os resolvers corretos. Isso transforma N+1 queries em 1+1.
2. "Look-ahead" e Join Strategy (Prisma e Modern ORMs)
Ferramentas modernas como o Prisma evoluíram. Antigamente, o Prisma sofria críticas por fazer muitas queries separadas. Agora, com a relationLoadStrategy: 'join', ele consegue forçar um SQL JOIN único no nível do banco de dados, sendo muito mais eficiente em ambientes de alta latência.
3. Soluções Híbridas (Breadth-First Loading)
Uma abordagem interessante vinda do WunderGraph (DataLoader 3.0) sugere resolver a árvore de dados em "largura" (Breadth-First) em vez de profundidade. Isso permite carregar todos os "autores" de todos os níveis da árvore de uma só vez, reduzindo a complexidade de concorrência e o uso de threads.
Resumo para Seniores (e aspirantes)
A diferença entre um Júnior e um Sênior muitas vezes não é saber sintaxe, é entender o custo de uma linha de código.
- Júnior: Faz o código funcionar. O endpoint retorna o JSON correto.
- Pleno: Usa
includeouselect_relatedno ORM para otimizar queries conhecidas. - Sênior: Implementa guardrails arquiteturais. Adiciona testes automatizados que falham se o número de queries exceder um limite (Query Count Budgeting) e monitora APMs (como Datadog ou New Relic) para pegar N+1 que escapou para produção.
Você já teve que refatorar uma API inteira por causa disso? Qual estratégia sua equipe usa hoje?
Fontes de referência:
-
Architect's dilemma: When to choose GraphQL over REST + why? - Hasura
◦ https://hasura.io/blog/architects-dilemma-when-to-choose-graphql-over-rest-and-why -
Solving the N+1 Problem with DataLoader - GraphQL.js
◦ https://www.graphql-js.org/docs/n1-dataloader/ -
Dataloader 3.0: A new algorithm to solve the N+1 Problem - WunderGraph
◦ https://wundergraph.com/blog/dataloader_3_0_breadth_first_data_loading