Skip to content

Artus Rocha

Performance: Problemas de consultas N+1

Em resumo, um problema de consultas N+1 ocorre quando para cada linha retornada em uma consulta inicial (à um banco de dados sql por exemplo) sua aplicação necessita realizar uma segunda consulta para recuperar dados adicionais de uma outra tabela. Vamos ver quais são suas implicações e como evitar este problema...


Vamos pensar numa aplicação hipotética de ecommerce, onde você precise listar todos os clientes (Customers) e seus respectivos pedidos (Orders). Uma implementação ingênua (tipicamente com um ORM) poderia sair assim:

SELECT * FROM "customers"

Por enquanto temos apenas uma consulta (esta consulta é o 1 do N+1) que retorna os Customers e então para obter os dados dos pedidos, serão necessárias N consultas adicionais, sendo N o numero de itens (Customers) retornados na primeira consulta.

-- Para cada customer
SELECT * FROM "orders" WHERE "orders"."customer_id" = 1
SELECT * FROM "orders" WHERE "orders"."customer_id" = 2
SELECT * FROM "orders" WHERE "orders"."customer_id" = 3
-- ... e assim por diante ...
  • Se a primeira consulta retornar 100 customers, teremos ao todo 101 consultas
  • Se a primeira consulta retornar 1000 customers, teremos ao todo 1001 consultas
    E assim por diante...

Mas é claro que sempre pode piorar, se por exemplo precisarmos incluir os Items/Produtos de cada pedido nos resultados...

ORM: Lazy vs Eager

A maioria dos ORMs usa por default uma abordagem chamada de lazy, onde a consulta retorna apenas a entidade diretamente consultada, sem incluir entidades relacionadas.
No entanto em sua maioria estes ORMs também permitirão a configuração de uma abordagem eager, nesta abordagem a consulta pode incluir um JOIN.
No nosso exemplo de Customers e Orders, uma configuração eager do ORM poderia resultar em uma unica consulta semelhante à esta:

SELECT * FROM "customers" as c
   LEFT JOIN "orders" ON "orders"."costumer_id" = c.id

Sim, isto retornaria os dados que precisamos em uma unica consulta. No entanto é necessário tomar cuidado com esta abordagem, pois ela pode sair muito cara.
Configurar o ORM para ter esta abordagem eager pode fazer com que ele sempre retorne os dados completos realizando joins, mesmo em outras partes da aplicação onde não precisamos.
Neste caso é preferivel verificarmos se o ORM em questão dispõe de mecanismos que nos permitam ativar/desativar este comportamento eager sob demanda em cada consulta.
Por exemplo em Java com JPA usando jpql temos a opção FETCH:

-- JPQL
SELECT * FROM "customers" JOIN FETCH "customers"."orders"

Uma abordagem simples

Uma abordagem talvez mais simples e que vai resolver muito bem este problema é realizar duas consultas como a seguir:

-- Ainda temos nossa consulta inicial que recupera os Customers
SELECT * FROM "customers"
-- E então realizamos uma segunda consula, passando os IDs obtidos na primeira,
-- que retornará todos os pedidos relacionados
SELECT * FROM "orders" 
   WHERE "orders"."customer_id" IN ( 1, 2, 3 /* ... cada customer_id */)

Concluindo

Existem muitas formas avançadas de otimização, mas antes de pensar em qualquer coisa mais extrema é essencial verificarmos se não estamos caindo em erros primários como consultas N+1.

Aqui me concentrei em consultas à base de dados relacionais, mas é valido pensarmos que os mesmos problemas podem se aplicar à cenários de APIs distribuidas por exemplo, como microsserviços onde precisamos chamar APIs secundárias para "enriquecer" os dados de nosso objeto de retorno.


Por hoje é isto pessoal...

abs!
Artus