DevGuideTK
← Trilha Backend

ORMs e o problema N+1

O que um ORM esconde, as consultas que ele gera sem avisar e como encontrá-las antes que deixem o sistema lento.

Resolver o N+1 é dizer ao ORM, na própria consulta, o que vai ser usado. As decisões são três: como os relacionamentos são carregados, qual comportamento padrão o ORM deve ter e como detectar o problema antes da produção.

Três formas de carregar relacionamentos

FormaConsultasComo funcionaQuando usar
Junção (JOIN)1Busca o pedido e o cliente na mesma consultaRelacionamentos para um: o cliente do pedido
Consulta separada com IN2Busca os pedidos e depois todos os clientes de uma vez: WHERE id IN (...)Coleções: os itens de cada pedido
Projeção1Pede só os campos que a tela usa, como pedido.id e cliente.nomeListagens e respostas de API

Por que a junção nem sempre é melhor

Para um relacionamento para um, como o cliente do pedido, a junção traz uma linha por pedido. É a escolha natural.

Para coleções, como os itens de cada pedido, a junção repete os dados do pedido em cada linha de item. Com duas coleções na mesma consulta, como itens e pagamentos, o resultado vira um produto cartesiano: um pedido com 10 itens e 3 pagamentos vira 30 linhas. Esse efeito se chama explosão cartesiana.

PedidosItens por pedidoPagamentos por pedidoLinhas com junçãoLinhas com consultas separadas
1001033.000100 + 1.000 + 300

Por isso os ORMs oferecem as duas estratégias, e a regra prática é: junção para relacionamentos para um, consultas separadas para coleções.

Projeção: a forma mais esquecida

Carregar entidades completas traz todas as colunas de todas as tabelas, inclusive textos longos que a tela não mostra. Uma projeção pede só o necessário:

SELECT p.id, p.status, c.nome
FROM pedidos p JOIN clientes c ON c.id = p.cliente_id

Projeções não carregam objetos rastreados pelo ORM, então servem para leitura, e não para alterar e salvar. Para listagens e respostas de API, costumam ser a opção mais rápida.

O comportamento padrão de cada ORM

Cada ORM tem uma postura diferente sobre o carregamento tardio, e isso muda onde o N+1 aparece.

ORMCarregamento tardio por padrãoOnde o N+1 costuma aparecer
Prisma (TS)Não existeLaços que fazem uma consulta por item
SQLAlchemy (Python)Sim, ao acessar o atributoAcesso a relacionamentos dentro de laços
GORM (Go)Não existeLaços que fazem uma consulta por item
Hibernate / JPA (Java)Coleções sim; relacionamentos para um são antecipados por padrãoColeções em laços, e consultas que trazem relacionamentos antecipados um a um
EF Core (C#)Não, salvo se ativadoLaços com consultas, ou carregamento tardio ativado

Dois casos merecem atenção:

  • JPA: @ManyToOne e @OneToOne são antecipados por padrão. Uma consulta que lista 100 pedidos pode disparar 100 consultas de clientes logo em seguida, sem nenhum acesso no código. A recomendação geral é marcar todos os relacionamentos como tardios e pedir o que precisa em cada consulta.
  • EF Core: sem carregamento tardio, um relacionamento não incluído chega vazio. A tentação é ativar o carregamento tardio para "resolver", o que troca um erro visível por um N+1 invisível.

Como detectar o N+1

TécnicaComo funcionaQuando pega o problema
Registrar o SQL em desenvolvimentoO ORM imprime cada consultaDurante o desenvolvimento, se alguém olhar
Contar consultas por requisiçãoUm middleware conta as consultas e avisa acima de um limiteEm desenvolvimento e nos testes
Teste que fixa o número de consultasO teste falha se uma tela passar a fazer mais consultasNa integração contínua, antes do deploy
Ferramentas de detecçãoBibliotecas que reconhecem a mesma consulta repetidaEm desenvolvimento
Rastreamento em produçãoFerramentas de APM mostram as consultas de cada requisiçãoDepois do deploy, com dados reais

A mais eficaz é o teste com número de consultas. Ele não depende de alguém lembrar de olhar o log e pega o problema no momento em que uma mudança o introduz, por exemplo quando alguém acrescenta um campo de relacionamento numa resposta.

Quando sair do ORM

O ORM não precisa ser usado em tudo. Quase todos permitem escrever SQL quando faz sentido:

SituaçãoMelhor caminho
Cadastros, alterações e leituras simplesORM
Listagens com relacionamentosORM com carregamento antecipado ou projeção
Relatórios com agregações e várias junçõesSQL escrito à mão, pelo próprio ORM ou por um construtor de consultas
Atualizações em massaUma instrução UPDATE ou DELETE, e não um laço de objetos
Consultas críticas de desempenhoSQL, conferido com EXPLAIN ANALYZE

Atualizações em massa são outro N+1 disfarçado: carregar 10 mil objetos, alterar um campo em cada um e salvar gera 10 mil UPDATE. Uma única instrução UPDATE ... WHERE faz o mesmo trabalho.

Resumo da decisão

  1. Peça os relacionamentos na consulta: junção para relacionamentos para um, consultas separadas para coleções.
  2. Prefira projeções em listagens e respostas de API.
  3. Desligue ou faça falhar o carregamento tardio, para que todo acesso seja explícito.
  4. Conte as consultas nos testes das telas e rotas mais usadas.
  5. Use SQL direto para relatórios e operações em massa.

O próximo nível mostra o problema e a correção no ORM mais usado de cada linguagem.

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Escolher a estratégia de carregamento

    Quero aprender a escolher entre junção, consulta separada com IN e projeção para carregar relacionamentos.
    
    Pergunte qual ORM eu uso.
    
    Explique as três estratégias, como pedir cada uma no meu ORM e o SQL que cada uma gera. Dê atenção especial à explosão cartesiana quando duas coleções são carregadas por junção.
    
    Depois me apresente estas telas, uma de cada vez, e pergunte qual estratégia eu usaria:
    1. Lista de pedidos com o nome do cliente.
    2. Detalhe de um pedido com itens, produtos de cada item e pagamentos.
    3. Uma API que devolve 1.000 produtos com a categoria, para outro sistema.
    4. Um relatório de total vendido por cliente no mês.
    5. Uma tela de edição de um pedido, que depois será salvo.
    
    Corrija cada resposta e explique o que daria errado com as outras estratégias.
  • Configurar o ORM para evitar N+1

    Quero configurar o meu ORM para que o N+1 seja difícil de acontecer, e não depender só de atenção.
    
    Pergunte qual linguagem, ORM e framework eu uso.
    
    Explique, para o meu caso:
    1. Qual é o comportamento padrão de carregamento dos relacionamentos, e quais armadilhas ele cria.
    2. Como fazer o carregamento tardio falhar com erro em vez de consultar o banco, se o meu ORM permitir.
    3. Como registrar o SQL gerado em desenvolvimento, de forma legível.
    4. Como contar as consultas por requisição e avisar acima de um limite.
    5. Quais bibliotecas de detecção de N+1 existem para o meu ecossistema.
    
    Termine com a configuração recomendada, pronta para eu aplicar.
  • Quando usar SQL em vez do ORM

    Quero saber quando vale a pena escrever SQL direto em vez de usar o ORM, sem abandonar o ORM no resto do sistema.
    
    Pergunte qual linguagem e ORM eu uso.
    
    Explique:
    1. Os tipos de operação em que o ORM ajuda e os tipos em que ele atrapalha.
    2. Como escrever SQL pelo próprio ORM, com parâmetros, mapeando o resultado para um tipo.
    3. Como fazer atualizações e exclusões em massa numa única instrução, e quais eventos e validações do ORM deixam de rodar nesse caso.
    4. Construtores de consultas como meio-termo entre o ORM e o SQL puro.
    
    Depois me apresente cinco operações e pergunte se eu usaria o ORM ou SQL em cada uma, corrigindo as minhas respostas.