Problema de N+1: O vilão silêncioso da Performance
O problema de N+1 muitas vezes passa despercebido por muitos desenvolvedores por ser um problema silencioso. Ele não gera erros no console, passa nos testes unitários com poucos dados, porém sua aplicação fica muito lenta quando o tráfego aumenta. A seguir irei disertar as seguintes questões sobre esse tema:
- O que é o Problema do N+1?
- Como ocorre este problema?
- E como resolver?
O que é o Problema do N+1?
O problema ocorre quando é executado uma consulta ao banco de dados que trás uma lista de itens (1), que após executa uma consulta adicional para cada um destes itens (N) para buscar os dados relacionados.
Pensando nisso, segue um exemplo abaixo para melhor compreensão:
Digamos que vamos realizar uma query no banco de dados para trazer usuário e após isso trazer seus respectivos posts.
- A consulta inicial (Buscando usuários)
SELECT * FROM users;
-- Retorna 50 usuários
- As consultas subsequentes (O"N")
SELECT * FROM posts WHERE user_id = 1;
SELECT * FROM posts WHERE user_id = 2;
SELECT * FROM posts WHERE user_id = 3;
-- assim por diante até o N
Neste cenário, para uma simples traréfa, você gerou "51 requisições" ao banco de dados. Digamos que cada requisição leve em média 10ms de latência de rede, você já gastou meio segundo somente conversando com o banco, sem contar o processamentos dos dados.
Como ocorre este problema?
Antes de tudo, irei dar uma breve explicação sobre Object Relational Mapping (ORM), como Prisma, Sequelize, Eloquent. Basicamente eles fazem o mapeamento dos objetos de uma linguagem de programação para querys SQL.
Na maior parte das vezes o maior vilão se tratando de N+1 é o Lazy Loading (carregamento tardio). É uma conveniência para questões de performance, porém pode ser tornar uma armadilha, fazendo o código parecer mais limpo, mas o custo de I/O de rede entre a aplicação e o banco de dados mata a performance.
const users = await db.users.findMany(); // 1 Query
users.map(user => {
// Aqui mora o perigo: cada iteração dispara uma nova query oculta
console.log(user.posts);
});
Como resolver o problema do N+1?
Enfim, vamos agora abordar a solução para este problema. E para isso iremos abordar as soluções mais comuns que melhoram a performance da aplicação, evitando o problema do N+1, segue abaixo as principais soluções:
- Eager Loading (O método mais comum)
Em vez de esperar o loop solicitar os dados, dizemos ao ORM para buscar os relacionamentos logo na primeira consulta. Na maioria dos frameworks modernos oferece métodos como includ, with ou join.
- A estratégia do
WHERE IN
Por trás dos panos, muitos ORMs resolvem o N+1 transformando aquelas "N" em apenas uma consulta adicional. Em vez de perguntar pelos posts de cada usuário individualmente, o sistema faz:
SELECT * FROM posts WHERE user_id IN (1, 2, 3, ..., 50);
- Utilizar os Joins
Se estiver lidando com um volume massivo de dados, a melhor opção é realizar um INNER JOIN ou LEFT JOIN diretamente no SQL. Isso trará o usuário e seus posts em uma única "tabela virtual" resultante.
SELECT users.*, posts.*
FROM users
LEFT JOIN posts ON users.id = posts.user_id;
Neste caso, o número de requisições cai para 1. Sendo está a solução mais performática em termos de I/O.
Conclusão
Meu conselho caro leitor é: monitore. Utilize ferramentas de Observabilidade e logs de queries em ambiente de desenvolvimento, pois são essenciais para a identificação do problema. Desta forma você irá evitar muita dor de cabeça com performance e processamento do banco de dados. Obrigado por ter chego até aqui e até mais!!!