01
Por que o teste passa: o N+1 é função dos dados, não do código
O padrão é sempre o mesmo: uma consulta traz a lista e, para cada linha, outra consulta traz algo relacionado. Com N linhas e k relações, o total é 1 + N × k consultas. Em um teste de unidade, N é pequeno porque o seed é pequeno, e cada consulta custa frações de milissegundo porque o banco roda na mesma máquina. Em produção, N é o que o cliente tem, e cada consulta paga a ida e volta até o banco gerenciado, que fica em outra máquina, em outra zona, com 0,5 a 2 milissegundos de latência. Como as consultas acontecem em série dentro do laço, esse tempo soma, não se sobrepõe.
É por isso que o problema é invisível em teste e em staging e aparece em produção. Nada falha: o resultado está correto, o teste passa, a revisão de código não vê nenhuma consulta lenta porque não existe consulta lenta. Existem mil consultas rápidas. O exemplo abaixo é a tela do caso, em Node com o driver pg, e vale igual para qualquer ORM que faça carregamento preguiçoso de relações.
// Versao com N+1: uma consulta para a lista e mais uma por pedido, por item e por evento
export async function listarPedidosDoDia(db, { porPagina = 100 } = {}) {
const { rows: pedidos } = await db.query(
`SELECT id, cliente_id, criado_em FROM pedidos
WHERE criado_em >= current_date ORDER BY criado_em DESC LIMIT $1`,
[porPagina],
);
for (const pedido of pedidos) {
const cliente = await db.query('SELECT id, nome FROM clientes WHERE id = $1', [pedido.cliente_id]);
pedido.cliente = cliente.rows[0] ?? null;
const itens = await db.query(
'SELECT id, produto_id, quantidade FROM itens_pedido WHERE pedido_id = $1',
[pedido.id],
);
for (const item of itens.rows) {
const produto = await db.query('SELECT id, nome, sku FROM produtos WHERE id = $1', [item.produto_id]);
item.produto = produto.rows[0] ?? null;
}
pedido.itens = itens.rows;
const ultimo = await db.query(
'SELECT status, ocorrido_em FROM eventos_pedido WHERE pedido_id = $1 ORDER BY ocorrido_em DESC LIMIT 1',
[pedido.id],
);
pedido.ultimoEvento = ultimo.rows[0] ?? null;
}
return pedidos;
}
// Seed de teste (10 pedidos, 2 itens cada): 1 + 10 + 10 + 20 + 10 = 51 consultas, 5 ms na rede local.
// Producao (100 pedidos, 9 itens em media): 1 + 100 + 100 + 900 + 100 = 1.201 consultas.| Ambiente | Pedidos na página | Itens por pedido | Consultas | Ida e volta ao banco | Tempo só de rede |
|---|---|---|---|---|---|
| Teste local (seed) | 10 | 2 | 51 | 0,1 ms | 5 ms |
| Staging | 30 | 4 | 211 | 0,4 ms | 84 ms |
| Produção, cliente médio | 100 | 9 | 1.201 | 0,8 ms | 961 ms |
| Produção, pico, pool de 10 disputado | 100 | 9 | 1.201 | 0,8 ms + espera por conexão | 3 a 6 s |
A última linha explica a parte mais grave do incidente. Cada uma das 1.201 consultas pega uma conexão do pool, usa por menos de um milissegundo e devolve. Com 40 usuários abrindo a tela no mesmo minuto, são 48 mil pedidos de conexão disputando 10 vagas. As outras telas, que fazem 3 consultas e deveriam responder em 20 milissegundos, ficam na fila atrás delas. O N+1 de uma tela vira a latência de todas.