O N+1: uma consulta que se multiplica sem se notar
É o problema de desempenho mais comum em qualquer aplicação com base de dados, e o mais difícil de ver a olho nu — porque o código que o causa parece perfeitamente razoável.
const artigos = await db.artigos.findMany({ take: 20 });
for (const artigo of artigos) {
artigo.autor = await db.autores.findUnique({ where: { id: artigo.autorId } });
}São 21 consultas: uma para a lista, mais uma por cada artigo. Daí o nome — N+1.
Com a base na mesma máquina, cada uma custa um milissegundo e ninguém nota. Com a base noutro servidor, cada uma custa dez, e a página passa a demorar duzentos milissegundos a mais do que devia. É sempre em produção que se descobre.
1. Pedir tudo de uma vez
Qualquer ORM tem forma de trazer as relações na mesma consulta:
const artigos = await db.artigos.findMany({
take: 20,
include: { autor: true },
});Uma consulta, ou duas — a diferença é entre um número fixo e um número que cresce com os resultados.
2. Quando não dá para juntar
Se os dados vierem de sítios diferentes, o padrão é recolher os ids e fazer uma consulta com IN:
const ids = [...new Set(artigos.map((a) => a.autorId))];
const autores = await db.autores.findMany({ where: { id: { in: ids } } });
const porId = new Map(autores.map((autor) => [autor.id, autor]));
for (const artigo of artigos) {
artigo.autor = porId.get(artigo.autorId);
}Duas consultas, independentemente de serem 20 artigos ou 20 000. O Set à volta dos ids evita pedir o mesmo autor cinco vezes.
3. Como se apanha
A olho, procura-se um await dentro de um ciclo. Em código real, o mais fiável é ligar o registo de SQL e olhar para o que sai:
// Prisma
new PrismaClient({ log: ['query'] });Se ao carregar uma página aparecerem vinte linhas quase iguais na consola, está encontrado. É um daqueles casos em que a ferramenta mostra em dez segundos o que uma revisão de código não vê.