Problema N+1 banco dados: causa, impacto e como resolver
O problema N+1 acontece quando uma consulta principal gera N consultas extras, uma para cada registro. Aprenda a detectar e corrigir com técnicas de eager loading e joins.
O problema N+1 acontece quando uma consulta principal gera N consultas extras, uma para cada registro. Aprenda a detectar e corrigir com técnicas de eager loading e joins.
Se você trabalha com ORM (Hibernate, Entity Framework, ActiveRecord), em algum momento esbarrou no problema N+1. O nome parece técnico, mas o fenômeno é simples: sua aplicação dispara uma consulta para listar N registros e, para cada um deles, faz outra consulta para buscar um dado relacionado. Resultado: N+1 idas ao banco. Em uma lista de 100 pedidos com 1 cliente cada, são 101 consultas. Em 10.000 pedidos, 10.001. O impacto em performance não é linear, é multiplicativo.
O pior é que o problema não aparece em ambiente de desenvolvimento, onde a base é pequena. Ele surge em produção, quando o volume cresce e a latência de rede entre aplicação e banco começa a pesar. Antes de otimizar, você precisa saber reconhecer o padrão.
Passo 1: Identifique o padrão N+1 no código
O sintoma clássico é um loop que acessa uma propriedade de relação dentro de uma iteração. Veja um exemplo em pseudocódigo com ORM:
pedidos = Pedido.where(status: "pago") pedidos.each do |pedido| puts pedido.cliente.nome end
Se o ORM usa lazy loading (carregamento preguiçoso), cada acesso a pedido.cliente dispara um SELECT separado. Para confirmar, ative o log de SQL do seu ORM e conte quantas queries são executadas. Um único SELECT inicial seguido de N SELECTs idênticos é a assinatura do problema.
Erro comum: achar que o problema é do banco. O banco está apenas respondendo às consultas que recebe. A ineficiência está na estratégia de acesso a dados da aplicação.
Passo 2: Substitua lazy loading por eager loading
A correção mais direta é forçar o carregamento antecipado dos relacionamentos. A maioria dos ORMs oferece métodos para isso.
No Rails/ActiveRecord, use includes:
pedidos = Pedido.includes(:cliente).where(status: "pago")
No Hibernate (Java), use JOIN FETCH ou @EntityGraph. No Entity Framework (C#), use Include:
var pedidos = context.Pedidos.Include(p => p.Cliente).Where(p => p.Status == "pago").ToList();
O ORM passa a montar uma única consulta com JOIN (ou duas consultas separadas, uma para pedidos e outra para clientes com IN). O número de idas ao banco cai de N+1 para 1 ou 2.
Dica: prefira a abordagem com duas queries e IN quando a relação for muito ampla, pois um JOIN gigante pode travar o banco. O ganho de performance vem de eliminar a repetição, não de forçar tudo em um SELECT.
Passo 3: Meça o antes e o depois
Não otimize no escuro. Antes de aplicar a mudança, meça o tempo total de execução da operação e o número de queries no log. Depois, repita a medição. A diferença costuma ser brutal: em um caso real com 500 registros, o tempo caiu de 2,3 segundos para 0,4 segundo após o eager loading. Números exatos variam conforme o ambiente, mas a ordem de grandeza é essa.
Erro comum: aplicar eager loading em todas as consultas, inclusive nas que não precisam. Isso aumenta o volume de dados trafegado sem necessidade. Use apenas onde o relacionamento é acessado dentro de loops.
Passo 4: Considere consultas customizadas para casos extremos
Quando a hierarquia de relacionamentos é profunda (pedido -> cliente -> endereço -> cidade), o eager loading pode gerar um JOIN monstruoso. Nesse cenário, escreva uma query SQL específica ou use um DTO (objeto de transferência) com apenas os campos necessários.
SELECT p.id, c.nome, c.email FROM pedidos p INNER JOIN clientes c ON c.id = p.cliente_id WHERE p.status = 'pago';
Isso elimina o N+1 e ainda reduz a carga de dados, já que você não carrega colunas que não vai usar.
Dica: se o ORM permite, use projeções ou select específico para não trazer objetos completos.
Checklist final
- [ ] Identifiquei o loop que acessa relacionamento e confirmei o N+1 no log de SQL
- [ ] Substituí lazy loading por eager loading nos pontos críticos
- [ ] Meço o tempo antes e depois da alteração
- [ ] Em hierarquias profundas, usei consulta customizada com JOIN
- [ ] Revisei outras consultas para não introduzir N+1 novo
O problema N+1 é um dos vilões mais comuns de performance em aplicações com banco relacional. A solução não é complexa, mas exige atenção ao padrão de acesso. Antes de comprar mais infraestrutura ou cache, verifique se suas queries estão enxutas.
Perguntas frequentes
O que exatamente é o problema N+1?
É quando uma consulta retorna N registros e, para cada um, o ORM executa uma consulta extra para carregar um relacionamento. No total, são N+1 consultas. Se você tem 200 pedidos e acessa o cliente de cada um, serão 201 SELECTs.
Como detectar o problema N+1?
Ative o log de SQL do seu ORM e observe se, após uma consulta inicial, aparecem muitas queries idênticas com parâmetros diferentes. Em Rails, a gem Bullet aponta automaticamente. Em Java, o Hibernate tem estatísticas que mostram o número de queries por sessão.
Qual a diferença entre lazy loading e eager loading?
Lazy loading carrega o relacionamento apenas quando ele é acessado, o que gera uma query por acesso. Eager loading carrega tudo de uma vez, geralmente com JOIN ou uma segunda consulta com IN, reduzindo o número total de idas ao banco.
O problema N+1 só acontece com ORM?
Não. Você pode escrever SQL manual e cair no mesmo padrão se fizer uma consulta dentro de um loop. O ORM apenas facilita o erro ao esconder as queries. A lógica é a mesma: evite consultas repetidas em iteração.
Eager loading sempre é a melhor solução?
Nem sempre. Se a relação não for usada, eager loading é desperdício. Em hierarquias profundas, um JOIN gigante pode ser pior. Avalie o contexto: use eager loading onde há repetição e consultas customizadas onde a projeção é complexa.
Letícia Sampaio Khoury
Editora de Gadgets e Consumo Tech
Testa o gadget no dia a dia real, avalia se vale a grana sem deslumbre de novidade.
Ver todos os artigos →