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.
O que um ORM faz
O código trabalha com objetos: um Pedido tem um Cliente e uma lista de Itens. O banco relacional trabalha com tabelas e chaves. Um ORM (Object-Relational Mapper) faz a ponte entre os dois mundos.
Em vez de escrever SQL e montar os objetos à mão, você declara as classes e os relacionamentos, e o ORM gera as consultas:
const pedido = await orm.pedido.buscarPorId(981);
console.log(pedido.cliente.nome);Por baixo, ele escreve algo como SELECT ... FROM pedidos WHERE id = 981, transforma a linha num objeto e, quando o código pede pedido.cliente, busca o cliente também.
Os ORMs mais usados: Prisma e TypeORM em TypeScript, SQLAlchemy e o ORM do Django em Python, GORM em Go, Hibernate e JPA em Java, Entity Framework Core em C#.
| O ORM dá | O ORM esconde |
|---|---|
| Menos código repetitivo | Quantas consultas são feitas |
| Tipos e autocompletar no editor | Qual SQL é gerado |
| Proteção contra injeção de SQL por padrão | Quando cada consulta é disparada |
| Migrações de esquema | O custo de cada acesso a um relacionamento |
A coluna da direita é a origem do problema deste tópico.
Carregar agora ou carregar depois
Quando o código busca um pedido, o ORM precisa decidir o que fazer com os relacionamentos dele. Há duas estratégias:
- Carregamento antecipado (eager loading): busca o pedido e o cliente juntos, de uma vez.
- Carregamento tardio (lazy loading): busca só o pedido. O cliente é buscado no momento em que o código acessa
pedido.cliente, com uma nova consulta.
O carregamento tardio parece econômico: só busca o que for usado. O problema aparece quando o acesso acontece dentro de um laço.
O problema N+1
A tela de pedidos mostra os 100 pedidos mais recentes, com o nome do cliente de cada um:
const pedidos = await orm.pedido.listar({ limite: 100 });
for (const pedido of pedidos) {
console.log(pedido.id, pedido.cliente.nome);
}O código tem uma consulta visível. O banco recebe 101:
SELECT * FROM pedidos ORDER BY id DESC LIMIT 100; -- 1 consulta
SELECT * FROM clientes WHERE id = 4242; -- e mais uma
SELECT * FROM clientes WHERE id = 117; -- para cada
SELECT * FROM clientes WHERE id = 8810; -- pedido...
-- ... 97 consultas iguaisÉ daí que vem o nome: 1 consulta para a lista e N consultas para os itens dela. O mesmo resultado sai de uma única consulta com junção:
SELECT p.id, c.nome
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
ORDER BY p.id DESC
LIMIT 100;Por que ninguém percebe
O N+1 passa pelos testes e pela revisão por três motivos:
- Em desenvolvimento, a lista é pequena. Com cinco pedidos, são seis consultas, e a tela abre na hora.
- O banco local responde rápido. Na mesma máquina, cada ida e volta leva uma fração de milissegundo.
- O código parece correto. Não há SQL à vista, e o acesso a
pedido.clienteparece só ler um campo.
Medimos a diferença no PostgreSQL, com 100 pedidos:
| Forma | Consultas | Banco na mesma máquina | Com atraso de rede simulado |
|---|---|---|---|
| N+1 | 101 | 15,9 ms | 227 ms |
Duas consultas, com IN | 2 | 1,0 ms | 5,4 ms |
Uma consulta, com JOIN | 1 | 0,8 ms | 2,1 ms |
Na mesma máquina, a diferença já é de quase 20 vezes, mas 16 milissegundos ainda parecem aceitáveis. Em produção, o banco fica em outra máquina, e cada consulta paga a ida e a volta pela rede. A coluna da direita acrescenta um pequeno atraso a cada consulta para simular isso: a tela passa de 2 para 227 milissegundos. Com várias listas na mesma página, ou com uma lista dentro de outra, o tempo chega a segundos.
O N+1 não é exclusivo de ORMs
O ORM facilita o problema, porque esconde as consultas, mas ele aparece em qualquer lugar onde um laço faz uma chamada por item:
- código sem ORM que chama
buscarCliente(id)dentro de umfor; - resolvedores GraphQL, em que cada campo é resolvido separadamente (o tópico de estilos de API mostra esse caso);
- chamadas a outro serviço por HTTP, uma por item da lista;
- leituras de cache, uma chave por vez em vez de várias de uma vez.
A forma de resolver é a mesma em todos: juntar as buscas numa só, ou em poucas.
Por que isso importa quando a IA escreve o código
Assistentes escrevem código com ORM fluente e idiomático, e é exatamente esse estilo que esconde o N+1:
- O relacionamento é acessado dentro do laço de montagem da resposta, com carregamento tardio.
- Um laço chama uma função auxiliar que, sem estar à vista, faz uma consulta.
- Para "corrigir" um erro de relacionamento não carregado, o carregamento tardio é ligado globalmente.
- A correção traz todas as colunas de todas as tabelas relacionadas, quando a tela usa duas.
O próximo nível compara as formas de carregar relacionamentos e as ferramentas para detectar o problema.
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.
N+1 em cinco minutos
Explique em no máximo cinco minutos de leitura o que é um ORM e o que é o problema N+1, para alguém que já usou um ORM mas nunca se preocupou com as consultas que ele gera. Cubra só o essencial: o que o ORM faz por mim, a diferença entre carregar relacionamentos agora ou depois, como um laço inocente vira centenas de consultas e por que isso só aparece em produção. Use o exemplo de uma lista de pedidos com o nome do cliente. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
Ver o SQL que o ORM gera
Quero entender o que o ORM faz por baixo, olhando as consultas que ele gera. Pergunte primeiro qual linguagem e qual ORM eu uso. Depois, conduza assim, uma etapa por vez: 1. Me mostre como ligar o registro das consultas SQL no meu ORM. 2. Monte comigo duas entidades, pedido e cliente, com o relacionamento entre elas. 3. Escreva uma busca de um pedido e o acesso ao cliente, e me peça para prever quantas consultas serão feitas antes de mostrar o SQL. 4. Faça o mesmo com uma lista de pedidos percorrida num laço. 5. Explique a diferença entre carregamento antecipado e tardio no meu ORM, e qual é o padrão dele. Em cada etapa, espere a minha previsão antes de mostrar o resultado. No final, peça que eu explique com minhas palavras por que o N+1 não aparece em desenvolvimento.
Encontrar o N+1 escondido
Quero treinar o olho para reconhecer o problema N+1 em código que não parece ter consultas. Pergunte qual linguagem e qual ORM eu uso. Depois me mostre oito trechos de código, um por vez, e pergunte se cada um tem N+1 e quantas consultas ele faz com uma lista de 100 itens. Inclua: - um acesso a relacionamento dentro de um laço; - uma função auxiliar que faz consulta, chamada num laço; - um serializador que acessa relacionamentos ao montar o JSON; - um N+1 aninhado com dois níveis; - um laço de atualizações que poderia ser uma instrução só; - dois trechos corretos, para eu não achar problema em tudo. Corrija cada resposta e mostre a versão corrigida quando houver problema.
O N+1 fora dos ORMs
Quero entender o problema N+1 como um padrão geral, e não só como um defeito de ORM. Explique como o mesmo problema aparece em: 1. Código sem ORM que faz uma consulta por item num laço. 2. Resolvedores GraphQL. 3. Chamadas HTTP a outro serviço, uma por item. 4. Leituras de cache, uma chave por vez. Para cada caso, mostre o código com o problema, estime o custo com uma lista de 100 itens e uma latência de rede realista, e mostre a correção (junção, IN, DataLoader, endpoint de lote ou leitura de várias chaves de uma vez). Faça uma pergunta de verificação depois de cada caso.