Como identificar e corrigir consultas N+1 no Rails

O N+1 é um anti-pattern, conhecido por ser uma abordagem ineficiente de realizar consultas em um banco de dados com um volume considerável de informações.

Consultas N+1 são um problema de desempenho comum em aplicações Rails, onde o banco de dados é consultado várias vezes por registros relacionados, resultando em tempos de resposta mais lentos.

O N+1 é um anti-pattern, conhecido por ser uma abordagem ineficiente de realizar consultas em um banco de dados com um volume considerável de informações.

Consultas N+1 são um problema de desempenho comum em aplicações Rails, onde o banco de dados é consultado várias vezes por registros relacionados, resultando em tempos de resposta mais lentos.

A razão para isso está na forma como o ActiveRecord funciona, que por padrão, é utilizado um “carregamento preguiçoso”, o que significa que ele carrega os registros associados somente quando eles são acessados. Isso pode levar a uma situação em que, por exemplo, uma lista de registros é carregada com uma única consulta. No entanto, depois, para cada um desses registros, ocorre uma consulta adicional para carregar seus registros associados.

Problemas causados por consultas N+1

Consequentemente, são executadas muitas consultas com o intuito de carregar esses registros associados. Tal cenário pode acarretar uma série de problemas, tais como:

  1. Aumento da Carga no Banco de Dados: Executar várias consultas para cada objeto individualmente aumenta a carga no servidor de banco de dados, levando a possíveis gargalos de desempenho.
  2. Latência de Rede: Cada consulta envolve comunicação entre a aplicação e o servidor de banco de dados, causando uma latência de rede. Isso pode desacelerar a aplicação, especialmente se o servidor de banco de dados não estiver localizado na mesma rede que o servidor da aplicação.
  3. Escalabilidade Reduzida: Na medida em que o software começa a escalar, ter mais usuários e/ou mais dados são adicionados, o problema se torna mais pronunciado. O grande número de consultas pode sobrecarregar o servidor de banco de dados e causar uma perda de desempenho geral na aplicação.

Estratégias para correção de consultas N+1

Consultas N+1 podem ser difíceis de serem detectadas e otimizadas. Muitas vezes, requer uma análise cuidadosa das consultas sendo executadas e dos relacionamentos de dados na aplicação para identificar onde as consultas N+1 estão ocorrendo e como otimizá-las.

Para mitigar de forma eficiente o problema recorrente da consulta N+1, é importante que os desenvolvedores adotem boas estratégias. Um exemplo notável consiste em:

  1. Eager Loading: Carrega todos os registros associados em uma única consulta, em vez de consultar o banco de dados para cada registro individualmente. Uma abordagem eficaz nesse contexto é a utilização do método includes, que viabiliza o carregamento simultâneo dos registros associados à consulta principal.
  2. Identificar as consultas N+1: Uma maneira de fazer isso é utilizar uma gem chamada bullet, que ajuda a detectar consultas N+1 em sua aplicação Rails.
  3. Batch Loading: Em vez de buscar dados relacionados um por um para cada objeto, o carregamento em lote carrega dados relacionados para um grupo de objetos em uma única consulta, melhorando a eficiência.
  4. Joins: Permite combinar registros relacionados em uma única consulta, em vez de consultar o banco de dados para cada registro individualmente. Você pode usar o método joins para especificar as associações a serem unidas.
  5. Manual Query Optimization: Em alguns casos, pode ser necessário otimizar manualmente as consultas usando SQL ou recursos especializados do ORM para buscar dados de maneira mais eficiente.

Resolvendo um cenário de consultas N+1

Por exemplo, se você possui uma lista de postagens e está exibindo o nome do autor para cada postagem, é possível que veja consultas repetidas para o registro do autor.

Digamos que você tenha um modelo de Post que pertence a um Autor:

class Author < ApplicationRecord
    has_many :posts
end

class Post < ApplicationRecord
    belongs_to :author
end

E você tem um show que exibe uma lista de postagens, juntamente com o nome do autor para cada postagem:

<% @posts.each  do |post| %>
  <div class="post">
    <h2><%= post.title %></h2>
    <p>By <%= post.author.name %></p>
  </div>
<% end %>

Se você carregar as postagens usando uma consulta simples @posts = Post.all no seu controller, isso resultará em uma consulta N+1, onde uma consulta é executada para carregar todas as postagens e N consultas adicionais são executadas para carregar o nome do autor para cada postagem.

Started GET "/posts" for 127.0.0.1 at 2023-08-15 14:30:00 -0300
Processing by PostsController #index as HTML
Post Load (1.0ms)  SELECT "posts".* FROM "posts"
Author Load (1.5ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 1]]
Author Load (1.0ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 2]]
Author Load (1.1ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 3]]
Author Load (1.2ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 4]]
Author Load (1.0ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 5]]
Author Load (1.1ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 6]]
Rendered posts/index.html.erb (10.0ms)
Completed 200 OK in 100ms (Views: 20.0ms | ActiveRecord: 10.0ms)

Se simplesmente utilizarmos uma das estratégias que nesse cenário o ideal seria o includes, facilmente teríamos um resultado bem positivo.

# Instead of this:
@posts = Post.all

# Use this:
@posts = Post.includes(:author).all

O método includes indica ao Rails para carregar o autor associado às postagens em uma única consulta, em vez de carregá-los um de cada vez (o que resultaria em consultas N+1).


Se estiver utilizando a gem Bullet, execute a sua aplicação Rails em modo de desenvolvimento e busque por alertas do Bullet no console. Se o Bullet detectar consultas N+1, ele exibirá um alerta como este, o que facilitará demais.

N+1 Query detected
Post => [:author]
Add to your finder: :includes => [:author]

Em suma, abordar a consulta N+1 é essencial para criar aplicações escaláveis e de alto desempenho, aptas a lidar com volumes de dados e cargas de usuários elevados sem sobrecarregar o banco de dados.

0 Comentário