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:
- 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.
- 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.
- 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:
- 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.
- 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.
- 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.
- 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.
- 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.