Novidades

N+1 query problem: o que é e como resolver de vez

ResumoO N+1 query problem é um gargalo de performance em bancos de dados que ocorre quando uma consulta inicial é seguida por consultas adicionais para cada registro relacionado, gerando excesso de queries. A solução envolve usar técnicas como eager loading, joins ou batch fetching para carregar todos os dados relacionados em uma única consulta, eliminando a repetição desnecessária e otimizando o desempenho do sistema.

O N+1 query problem acontece quando seu banco de dados executa uma consulta para cada registro relacionado, gerando dezenas ou milhares de queries desnecessárias. Saiba como identificar e resolver esse gargalo clássico de performance.

Igor Bastos
N+1 query problem: o que é e como resolver de vez

N+1 query problem: o que é e como resolver de vez — Foto: Reprodução / Blog Sem Juízo

O que é o N+1 query problem e por que ele é um problema real?

O N+1 query problem é um padrão de acesso a dados em que uma aplicação executa uma consulta SQL (a primeira, o "1") para buscar uma lista de registros e, em seguida, para cada registro dessa lista, executa uma consulta adicional (as N queries) para carregar dados relacionados. Se você tem 100 posts no blog e busca o autor de cada um, são 101 consultas no total: uma para os posts e 100 para os autores. Em uma lista de 500 pedidos com itens, o número salta para 501.

O problema não é o volume absoluto de dados, mas a quantidade de idas e vindas ao banco. Cada query tem overhead de rede, parsing de SQL, planejamento de execução e retorno de resultado. Multiplicar isso por N faz a aplicação gastar segundos extras, ou minutos, sem necessidade. Em sistemas com ORM (como ActiveRecord, Hibernate, Entity Framework), o N+1 é traiçoeiro porque o código parece inocente: você escreve um loop simples e o ORM gera as queries extras nos bastidores, sem aviso.

Como identificar o N+1 query problem no seu código?

A detecção começa com observação. Se uma página que lista registros demora mais do que o esperado, desconfie. Ferramentas como o Rails Panel (para Ruby on Rails), Django Debug Toolbar (para Django) ou Hibernate Statistics (para Java) mostram todas as queries executadas em uma requisição. O padrão é claro: uma query para a tabela principal, seguidas por N queries idênticas para outra tabela, variando apenas o ID na cláusula WHERE.

Outro sinal: logs de banco cheios de consultas repetitivas. Em PostgreSQL ou MySQL, ative o log lento de queries (slow query log) e veja se há dezenas de consultas curtas para a mesma tabela. Em APIs REST, um endpoint que retorna uma lista de recursos e depois busca detalhes de cada um individualmente (por exemplo, /posts seguido de N chamadas a /posts/:id/comments) também é N+1, só que em nível HTTP.

Qual a diferença entre lazy loading e eager loading?

Lazy loading (carregamento preguiçoso) posterga a busca de dados relacionados até o momento em que eles são acessados. É o comportamento padrão da maioria dos ORMs. Se você itera sobre uma lista de categorias e, para cada uma, acessa categoria.produtos, o ORM dispara uma nova query por categoria. Isso economiza memória quando você não usa a relação, mas é a causa direta do N+1 quando você usa.

Eager loading (carregamento antecipado) resolve o problema ao trazer todos os dados relacionados em uma única consulta, usando JOIN ou uma segunda query com WHERE id IN (...). No ActiveRecord, você usa includes(:produtos); no Entity Framework, Include("Produtos"); no Hibernate, JOIN FETCH. O banco faz menos viagens, e a aplicação ganha velocidade. A troca é simples: mais dados por query versus menos queries no total.

Como resolver o N+1 query problem com joins?

O método mais direto é usar JOIN SQL. Em vez de buscar posts e depois, para cada post, buscar o autor, você escreve:

SELECT posts., autores. FROM posts JOIN autores ON autores.id = posts.autor_id;

Isso retorna tudo em uma query. O banco processa uma única varredura, e a aplicação recebe um resultado completo. A desvantagem: se um post não tiver autor, o INNER JOIN o exclui. Use LEFT JOIN se quiser posts sem autor também. O JOIN funciona bem quando a relação é obrigatória e o volume de dados cabe na memória.

Cuidado com joins em cascata. Se você juntar posts, autores, comentários e curtidas em uma query só, o resultado pode explodir em linhas (produto cartesiano). Nesse caso, prefira múltiplas queries com WHERE id IN (...), que ainda são poucas queries (uma por tabela) e não geram repetição de dados.

Quais ferramentas ajudam a prevenir o N+1 query problem?

Ferramentas de análise estática e gems de detecção ajudam a pegar o problema antes de ir para produção. Algumas opções:

  • Bullet (Ruby on Rails): monitora queries N+1 durante o desenvolvimento e avisa no console ou no navegador.
  • nplusone (Python, para SQLAlchemy e Django): detecta carregamentos preguiçosos desnecessários e sugere eager loading.
  • Hibernate query plan cache (Java): permite visualizar o plano de execução e identificar múltiplas queries repetidas.
  • New Relic, Datadog ou AppSignal: em produção, mostram a contagem de queries por requisição e destacam endpoints com N+1.

Nenhuma ferramenta substitui a revisão de código. Mas elas reduzem a chance de um N+1 passar despercebido em code review.

O N+1 query problem só acontece com ORM?

Não. O N+1 é um padrão de acesso a dados, não um bug de ORM. Você pode escrevê-lo manualmente em SQL puro: um cursor que busca clientes e, dentro de um loop, executa SELECT * FROM pedidos WHERE cliente_id = ? para cada um. O ORM apenas automatiza o padrão, tornando-o mais fácil de cair sem perceber.

Em APIs REST, o mesmo problema aparece quando o front-end faz uma requisição para listar itens e depois N requisições para buscar detalhes de cada item. A solução é criar um endpoint que retorne os dados agregados de uma vez, ou usar GraphQL com batching (como o DataLoader do JavaScript).

Quando o N+1 não é um problema?

Em listas muito pequenas (menos de 10 registros) e com baixa concorrência, o N+1 pode ser irrelevante. Se você tem uma página de admin que mostra 5 categorias e cada uma carrega 3 produtos, o overhead de 6 queries extras é desprezível. O problema escala quando a lista cresce: 100 registros viram 101 queries, 1.000 viram 1.001. O ponto de corte depende da latência do banco e da frequência da requisição, mas, em geral, acima de 20 registros já vale a pena otimizar.

Outro caso: quando a relação é opcional e raramente acessada, o lazy loading pode ser mais eficiente que trazer dados que nunca serão usados. A decisão é de engenharia, não de dogma.

Resumo e próximo passo

O N+1 query problem é um dos gargalos mais comuns em aplicações que usam banco de dados relacional. Identifique-o monitorando queries repetidas, resolva com eager loading (joins ou queries com IN), e previna com ferramentas de detecção. O próximo passo prático: habilite o log de queries no seu ORM, carregue uma página que lista registros e conte quantas queries foram executadas. Se o número for maior que o esperado, você já sabe o que fazer.

FAQ, Perguntas frequentes sobre N+1 query problem

O que é o problema N+1 em SQL?

É a execução de uma query para buscar uma lista de registros seguida de N queries individuais para buscar dados relacionados de cada registro. Exemplo: 1 query para listar 100 clientes + 100 queries para buscar os pedidos de cada cliente = 101 queries no total.

Como evitar o N+1 query problem no Django?

Use o método select_related() para relações ForeignKey e prefetch_related() para ManyToMany. Ambos forçam o eager loading e reduzem o número de queries. Exemplo: Post.objects.select_related('autor').all() carrega autores junto com os posts em uma query.

Como resolver o N+1 no Entity Framework?

Use o método Include() para especificar relações que devem ser carregadas na mesma query. Exemplo: context.Posts.Include(p => p.Autor).ToList(). Para múltiplas relações, encadeie Includes ou use ThenInclude().

O que causa o N+1 query problem?

A causa direta é o lazy loading combinado com acesso a dados relacionados dentro de um loop. O ORM, ao encontrar post.autor.nome dentro de um for, dispara uma query para cada post. A causa indireta é a falta de planejamento de acesso a dados, escrever o código pensando em um único registro e depois aplicá-lo a uma lista.

N+1 é um problema só de banco relacional?

Não. O padrão aparece em qualquer sistema onde você busca uma lista e depois busca detalhes individuais: APIs REST, chamadas a serviços externos, consultas a bancos NoSQL (se você fizer um get por chave em loop). A lógica de resolver é sempre a mesma: agrupe as requisições em uma única chamada.

Igor Bastos

Editoria Novidades

Igor Bastos cobre o setor de meios de pagamento e crédito no Blog Sem Juízo. Análises técnicas, sem viés comercial.

Leia também · Novidades