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
| Forma | Consultas | Como funciona | Quando usar |
|---|---|---|---|
Junção (JOIN) | 1 | Busca o pedido e o cliente na mesma consulta | Relacionamentos para um: o cliente do pedido |
Consulta separada com IN | 2 | Busca os pedidos e depois todos os clientes de uma vez: WHERE id IN (...) | Coleções: os itens de cada pedido |
| Projeção | 1 | Pede só os campos que a tela usa, como pedido.id e cliente.nome | Listagens 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.
| Pedidos | Itens por pedido | Pagamentos por pedido | Linhas com junção | Linhas com consultas separadas |
|---|---|---|---|---|
| 100 | 10 | 3 | 3.000 | 100 + 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_idProjeçõ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.
| ORM | Carregamento tardio por padrão | Onde o N+1 costuma aparecer |
|---|---|---|
| Prisma (TS) | Não existe | Laços que fazem uma consulta por item |
| SQLAlchemy (Python) | Sim, ao acessar o atributo | Acesso a relacionamentos dentro de laços |
| GORM (Go) | Não existe | Laços que fazem uma consulta por item |
| Hibernate / JPA (Java) | Coleções sim; relacionamentos para um são antecipados por padrão | Coleções em laços, e consultas que trazem relacionamentos antecipados um a um |
| EF Core (C#) | Não, salvo se ativado | Laços com consultas, ou carregamento tardio ativado |
Dois casos merecem atenção:
- JPA:
@ManyToOnee@OneToOnesã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écnica | Como funciona | Quando pega o problema |
|---|---|---|
| Registrar o SQL em desenvolvimento | O ORM imprime cada consulta | Durante o desenvolvimento, se alguém olhar |
| Contar consultas por requisição | Um middleware conta as consultas e avisa acima de um limite | Em desenvolvimento e nos testes |
| Teste que fixa o número de consultas | O teste falha se uma tela passar a fazer mais consultas | Na integração contínua, antes do deploy |
| Ferramentas de detecção | Bibliotecas que reconhecem a mesma consulta repetida | Em desenvolvimento |
| Rastreamento em produção | Ferramentas de APM mostram as consultas de cada requisição | Depois 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ção | Melhor caminho |
|---|---|
| Cadastros, alterações e leituras simples | ORM |
| Listagens com relacionamentos | ORM com carregamento antecipado ou projeção |
| Relatórios com agregações e várias junções | SQL escrito à mão, pelo próprio ORM ou por um construtor de consultas |
| Atualizações em massa | Uma instrução UPDATE ou DELETE, e não um laço de objetos |
| Consultas críticas de desempenho | SQL, 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
- Peça os relacionamentos na consulta: junção para relacionamentos para um, consultas separadas para coleções.
- Prefira projeções em listagens e respostas de API.
- Desligue ou faça falhar o carregamento tardio, para que todo acesso seja explícito.
- Conte as consultas nos testes das telas e rotas mais usadas.
- 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.