O que é o problema de consultas N+1?

Atualizado em: agosto de 2026

O problema de consultas N+1 é um padrão em que buscar N registros dispara uma query extra por registro — N+1 idas ao banco em vez de uma ou duas. É o bug sério de performance mais comum em aplicações apoiadas em dados, e o mais camuflado: cada query individual é rápida, o código lê perfeitamente, e a página funciona — até o dia em que os volumes reais de dados chegam.

Principais pontos

PerguntaResposta
O formato1 query para a lista + N queries para relações = N+1 idas ao banco
A causaLazy loading tocado dentro de um loop — queries invisíveis por iteração
A assinaturaMuitas queries rápidas e idênticas — logs de queries lentas nunca a veem
As saídasEager loading · joins · IN em batch · loaders por requisição
O multiplicadorLatência de ida e volta × tamanho da página — brutal sobre a rede

O bug, às claras

// 1 query: busca 100 posts
const posts = await postRepo.findRecent(100);

for (const post of posts) {
  // +1 query POR POST — post.author parece uma propriedade,
  // mas o lazy loading roda: SELECT * FROM users WHERE id = ?
  render(post.title, (await post.author).name);
}
// Log de queries: 1 + 100 = 101 idas ao banco para renderizar uma página

A correção, como os SDKs a expressam — declare a relação de antemão, e a plataforma a busca na mesma requisição:

// JavaScript / Node.js — Back4app JS SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
const query = new Parse.Query('Comment');
query.equalTo('post', post);
query.include('author');                    // eager-load the pointer
const comments = await query.find();        // one round trip, total

comments.forEach((c) =>
  render(c.get('text'), c.get('author').get('username')) // already loaded
);

O custo, na ponta do lápis

A aritmética que os explicadores pulam — tempo total ≈ query da lista + (N × latência de ida e volta):

Tamanho da página N@1 ms/query@5 ms/queryEm batch (1–2 queries)
10~11 ms~55 ms~10 ms
100~101 ms~505 ms~12 ms
1.000~1,0 s~5,0 s~20 ms

Correções publicadas batem com a matemática: páginas caindo de 1,4 s para 0,16 s, endpoints acelerando 30× ou mais. Dois corolários que valem fixar: paginação limita N — um tamanho de página limitado limita o raio da explosão antes mesmo da correção de verdade; e a versão em rede é pior — quando cada uma das N chamadas é uma requisição HTTP em vez de uma query local, multiplique por dezenas de milissegundos em vez de um dígito.

As quatro saídas

Cascata N mais 1 versus busca em batchO padrão N+1 dispara uma query de lista seguida de uma query por registro em sequência; o padrão em batch dispara a query de lista e uma única query buscando todos os registros relacionados de uma vez.

a correção

Em batch

Query da lista

UMA query para todas as relações
WHERE id IN (…)

Cascata N+1

Query da lista

Query por registro
× N, uma após a outra

O padrão N+1 dispara uma query de lista seguida de uma query por registro em sequência; o padrão em batch dispara a query de lista e uma única query buscando todos os registros relacionados de uma vez.
CorreçãoComoEscolha quandoCuidado com
Eager loading / includeDeclare as relações na queryA tela sempre precisa da relaçãoIncluir demais incha os payloads
JoinUma instrução busca os doisMotor relacional, leituras em formato de relatórioExplosão de linhas em joins largos
IN em batchColete as chaves, busque uma vezQualquer stack, até feito à mãoUma ida extra ao banco (aceitável)
Loader por requisiçãoBatch automático na execuçãoResolvers GraphQL, código em camadasPrecisa ser por requisição, não global

A última linha é o caso famoso do GraphQL: resolvers disparam por objeto pai, recriando o loop no servidor, e o padrão loader — coletar chaves num tick, buscar uma vez, memoizar por requisição — é a cura padrão da indústria. Uma regra sutil vem junto: loaders têm escopo de requisição; um global vira um cache velho com bugs de autorização.

Detecção: cace as queries rápidas

A assinatura do N+1 é o inverso do trabalho normal de performance: você procura muitas queries rápidas e idênticas, não uma lenta — motivo pelo qual logs de queries lentas, a ferramenta usual, nunca a veem. Os métodos que veem: log de debug do ORM (a mesma instrução, N parâmetros diferentes, uma renderização); as visões de spans do APM, onde a cascata de spans curtos idênticos é inconfundível; e o mais barato de todos — conte as queries de um carregamento de página em desenvolvimento, com um limiar na cabeça: uma página de lista deve custar queries em um dígito, não em múltiplos das suas linhas. O gêmeo do lado da escrita merece a mesma auditoria: um loop de insert por item é o N+1 das escritas, corrigido com operações em bulk.

Casos de uso comuns

  • Listas com autores, donos ou status — o lar canônico: todo feed, inbox e tabela que junta pessoas a itens.
  • APIs GraphQL — campos de lista aninhados são estruturalmente N+1 até existirem loaders.
  • Bancos de documentos — um find por documento referenciado; corrigido com include, IN em batch ou embutindo, conforme as regras de modelagem.
  • Fan-outs de microsserviços — uma lista do serviço A, uma chamada HTTP ao serviço B por item; endpoints compostos e BFFs existem para acabar com isso.
  • Jobs em background — o loop que processa 10.000 registros com duas queries cada, custando horas em silêncio.

Lazy vs. eager loading: matriz de decisão

Carregue eager quando…Fique no lazy quando…
A relação renderiza em toda linhaA relação está atrás de um clique
O loop é o padrão de acessoO acesso é um registro por vez
N é do tamanho da página ou maiorN é garantidamente minúsculo
A latência é visível ao usuárioUma tarefa em background tolera a espera
Você acabou de corrigir este bug aquiVocê mediu, em vez de supor

E a regra de auditoria permanente que sobrevive a qualquer matriz: toda relação tocada dentro de um loop é culpada até o log de queries provar o contrário.

Limitações e trade-offs

  • Eager loading pode corrigir demais. Incluir relações pesadas em todo lugar troca N+1 por payloads inchados e joins largos — inclua o que a tela renderiza, não o grafo inteiro.
  • Joins têm seu próprio penhasco. Joins um-para-muitos duplicam as linhas do pai por filho; com fan-out alto, a busca em batch de duas queries vence o join único.
  • Loaders adicionam maquinário. Escopo por requisição, invalidação de cache dentro da requisição e janelas de batching são código de verdade — o preço do batch automático.
  • Frameworks o reintroduzem em silêncio. Serializers, helpers de template e reviews de “só mais um campo” são como páginas corrigidas regridem; a checagem de contagem de queries pertence ao CI, não à memória.
  • A correção é por caminho, não global. N+1 é um bug de padrão de acesso; cada tela nova refaz a pergunta.

O N+1 no Back4app

O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. Seus SDKs fazem da correção o idioma, não a remediação: relações são Pointers tipados, e o include() — as abas de código acima — as busca na mesma requisição, agrupadas no servidor. A API GraphQL resolve queries aninhadas sem fan-out por campo, e os padrões de paginação limitam N antes de ele criar dentes. O loop que causa o N+1 não tem forma natural de ser escrito — que é o melhor tipo de correção: a que ninguém precisa lembrar.

Perguntas frequentes

O que é o problema de consultas N+1, em termos simples?

Você busca uma lista de N registros com uma query, e aí o seu código faz mais uma query por registro para os dados relacionados — cem posts viram cento e uma queries. Cada query é individualmente rápida, e é exatamente por isso que o problema se esconde: nada é lento o bastante para alarmar alguém, até a página fazer centenas de idas ao banco.

O que causa queries N+1?

Lazy loading como padrão. ORMs e SDKs deixam você navegar relações como propriedades de objeto — post.author — e rodam uma query de forma transparente quando você toca numa delas. Coloque esse acesso dentro de um loop sobre N resultados e você escreveu N queries em silêncio. A abstração que tornou o acesso a dados agradável também tornou as idas ao banco invisíveis.

Como corrigir o problema N+1?

Quatro saídas padrão, escolhidas caso a caso: eager loading — avisar à query, de antemão, para incluir a relação; um join que busca os dois numa instrução só; batching — coletar as N chaves estrangeiras e buscá-las numa única query de contido-em; e loaders com escopo de requisição, que agrupam automaticamente. As quatro transformam N+1 idas ao banco em uma ou duas.

Quão mais lento o N+1 é de verdade?

Faça a conta: cada ida ao banco custa de um a cinco milissegundos antes de a query sequer rodar, então 100 linhas a 5 ms somam meio segundo, contra uma query em batch de ~10 ms. Correções publicadas do mundo real relatam páginas indo de cerca de 1,4 segundo para 0,16, e endpoints de API acelerando trinta vezes. A matemática piora linearmente com o tamanho da página — e catastroficamente sobre a rede.

Por que o problema N+1 é tão comum em GraphQL?

Porque resolvers rodam por campo, por objeto. Uma query de posts com seus autores roda o resolver de posts uma vez e o resolver de autor N vezes — o loop de ORM renascido no servidor. A cura canônica é o padrão loader: um agrupador com escopo de requisição que coleta os IDs de autor durante a execução e dispara uma busca em batch, memoizada para a requisição.

Como detectar queries N+1?

Procure muitas queries idênticas e rápidas, não uma lenta — essa é a assinatura. O log de debug do ORM mostra a mesma instrução repetida com parâmetros diferentes; logs de queries lentas erram o alvo por completo, porque cada query é rápida; ferramentas de APM sinalizam o padrão de spans explicitamente. O hábito que pega cedo: leia o log de queries de uma renderização de página, e conte.

O problema N+1 acontece em bancos de documentos e APIs REST?

Em todo lugar onde dados têm relações. Em bancos de documentos, é um find por documento referenciado — resolvido com mecanismos de include, queries de contido-em em batch, ou embutindo. Sobre REST, é um endpoint de lista mais uma chamada HTTP por item — pior que a versão de banco, porque a latência de rede engole a latência de query; endpoints compostos, endpoints de batch e linguagens de consulta existem em boa parte para matar isso.

Lazy loading é sempre errado?

Não — ele é errado dentro de loops. Lazy loading é exatamente certo quando os dados relacionados raramente são necessários: pague por eles no único registro que precisa, em vez de pagar de forma eager por todos os N. A disciplina é saber o padrão de acesso de cada tela: relações sempre necessárias carregam eager, as raramente necessárias carregam lazy, e qualquer coisa dentro de um loop passa por auditoria.

Termos relacionados

Compare com

Leitura adicional

Pronto para construir seu backend?

Comece seu projeto no Back4app em minutos — banco de dados, autenticação, APIs e Cloud Code incluídos. Sem cartão de crédito.

Escrito e revisado por Back4app Engineering, Back4app Engineering · Publicado em 2026-08-21