ActiveRecord: Resolvendo problemas de consultas N+1 em Ruby on Rails
Guia completo para detectar e corrigir consultas N+1 em Rails com ActiveRecord. Domine includes, preload, eager_load e ferramentas de detecção automática.

As consultas N+1 representam um dos problemas de desempenho mais comuns em aplicações Rails. Um simples loop sobre registros pode disparar centenas de consultas SQL desnecessárias, retardando drasticamente os tempos de resposta. Este guia aborda as técnicas de detecção e resolução para garantir aplicações Rails performáticas.
Uma página exibindo 50 artigos com seus autores pode gerar 51 consultas SQL em vez de apenas uma. Em produção com milhares de usuários, esse problema se torna crítico para os tempos de resposta e a carga do servidor.
Compreendendo o problema N+1
O problema N+1 ocorre quando o código executa uma consulta para recuperar uma lista de registros (1 consulta), e em seguida executa uma consulta adicional para cada registro a fim de acessar suas associações (N consultas). O nome "N+1" descreve exatamente esse padrão: 1 consulta inicial + N consultas para as associações.
Um exemplo concreto com artigos e seus autores ilustra o problema. Sem otimização, cada acesso ao autor de um artigo dispara uma nova consulta SQL.
# app/controllers/articles_controller.rb
# Example code generating an N+1 problem
class ArticlesController < ApplicationController
def index
# 1 query: SELECT * FROM articles
@articles = Article.all
end
endNa view, cada chamada a article.author dispara uma consulta adicional ao banco de dados.
<!-- app/views/articles/index.html.erb -->
<!-- This view generates N additional queries -->
<% @articles.each do |article| %>
<div class="article">
<h2><%= article.title %></h2>
<!-- Each call generates: SELECT * FROM users WHERE id = ? -->
<p>By <%= article.author.name %></p>
</div>
<% end %>Para 100 artigos, esse código gera 101 consultas SQL. Os logs do Rails mostram claramente o problema com consultas repetitivas.
-- Rails logs showing the N+1 problem
-- 1 initial query
SELECT "articles".* FROM "articles"
-- N queries for authors (repeated for each article)
SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
SELECT "users".* FROM "users" WHERE "users"."id" = 3 LIMIT 1
-- ... 97 more queriesResolvendo com includes
O método includes é a solução mais comum e recomendada para corrigir problemas N+1. Ele instrui o ActiveRecord a pré-carregar as associações em uma ou duas consultas otimizadas.
# app/controllers/articles_controller.rb
# Solution with includes - preloading authors
class ArticlesController < ApplicationController
def index
# Preloads authors with articles
# Generates only 2 queries instead of N+1
@articles = Article.includes(:author).all
end
endCom includes, o ActiveRecord executa apenas duas consultas, independentemente do número de artigos. A primeira recupera todos os artigos, a segunda recupera todos os autores relevantes.
-- Rails logs with includes (only 2 queries)
SELECT "articles".* FROM "articles"
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5, ...)Associações aninhadas também podem ser pré-carregadas usando a sintaxe de hash. Essa abordagem é essencial quando as views acessam vários níveis de associações.
# app/controllers/articles_controller.rb
# Preloading nested associations
class ArticlesController < ApplicationController
def index
# Preloads author -> company and all comments
@articles = Article.includes(author: :company, comments: :user)
end
endSe uma view acessa uma associação dentro de um loop, essa associação deve ser pré-carregada no controller com includes. Verifique sempre os padrões de acesso a associações nas views.
Diferenças entre includes, preload e eager_load
O Rails fornece três métodos para pré-carregamento de associações. Cada um utiliza uma estratégia SQL diferente, com casos de uso específicos.
preload: consultas separadas
O método preload sempre executa consultas separadas para cada associação. Funciona com eficiência quando nenhuma condição WHERE filtra sobre as associações.
# app/models/article.rb
# preload always uses separate queries
class Article < ApplicationRecord
scope :with_authors, -> { preload(:author) }
end
# Usage in controller
@articles = Article.with_authors.limit(20)
# SQL generated:
# SELECT "articles".* FROM "articles" LIMIT 20
# SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, ...)eager_load: LEFT OUTER JOIN
O método eager_load utiliza um LEFT OUTER JOIN para carregar os dados em uma única consulta. Torna-se obrigatório ao filtrar ou ordenar por colunas de associações.
# app/controllers/articles_controller.rb
# eager_load allows filtering on associations
class ArticlesController < ApplicationController
def verified_authors
# Filters articles by author status
# Requires eager_load because WHERE references users
@articles = Article.eager_load(:author)
.where(users: { verified: true })
.order("users.name ASC")
end
end
# SQL generated (single query with JOIN):
# SELECT "articles"."id", "articles"."title", ...
# FROM "articles"
# LEFT OUTER JOIN "users" ON "users"."id" = "articles"."author_id"
# WHERE "users"."verified" = TRUE
# ORDER BY "users"."name" ASCincludes: comportamento inteligente
O método includes escolhe automaticamente a melhor estratégia. Usa preload por padrão, mas alterna para eager_load se uma cláusula WHERE referenciar a associação.
# app/controllers/articles_controller.rb
# includes adapts automatically to context
class ArticlesController < ApplicationController
def index
# No condition on association: uses preload (2 queries)
@articles = Article.includes(:author).all
end
def by_verified_authors
# With condition on association: uses eager_load (JOIN)
@articles = Article.includes(:author)
.where(users: { verified: true })
end
endA tabela a seguir resume as diferenças entre os três métodos.
| Método | Estratégia SQL | Caso de uso |
|--------|----------------|-------------|
| preload | Consultas separadas | Pré-carregamento simples, sem filtragem |
| eager_load | LEFT OUTER JOIN | Filtragem/ordenação em associações |
| includes | Automático | Uso geral, padrão recomendado |
Detecção automatizada de N+1
A detecção manual de problemas N+1 é tediosa e propensa a erros. Várias ferramentas automatizam essa detecção em desenvolvimento e CI.
Bullet: detecção em tempo real
A gem Bullet analisa as consultas SQL em tempo real e alerta sobre problemas N+1 detectados. Ela também sugere as correções apropriadas.
# Gemfile
# Bullet detects N+1 in development
group :development do
gem 'bullet'
endA configuração no ambiente de desenvolvimento permite ativar diversos modos de alerta.
# config/environments/development.rb
# Bullet configuration to detect N+1
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
# Display JavaScript alert in browser
Bullet.alert = true
# Add footer with details
Bullet.bullet_logger = true
# Display in Rails logs
Bullet.rails_logger = true
# Raise exception (useful in CI)
Bullet.raise = false
end
endQuando um problema N+1 é detectado, o Bullet exibe uma mensagem explícita com a solução recomendada.
# Example Bullet alert in logs
USE eager loading detected
Article => [:author]
Add to your query: .includes([:author])
Call stack:
/app/views/articles/index.html.erb:5Na integração contínua, ativar Bullet.raise = true faz com que os testes falhem se um problema N+1 for detectado. Isso evita regressões de desempenho.
Prosopite: alternativa leve
A gem Prosopite oferece uma alternativa mais leve ao Bullet, com configuração mínima e compatibilidade com testes.
# Gemfile
# Prosopite as an alternative to Bullet
group :development, :test do
gem 'prosopite'
end# config/environments/development.rb
# Prosopite configuration
Rails.application.configure do
config.after_initialize do
Prosopite.rails_logger = true
Prosopite.raise = Rails.env.test?
end
endTécnicas avançadas de otimização
Além dos métodos básicos, várias técnicas permitem ajustar com precisão a otimização das consultas ActiveRecord.
Strict Loading: prevenção por padrão
O Rails 6.1+ fornece um modo de carregamento estrito que lança uma exceção se uma associação não pré-carregada for acessada. Essa abordagem preventiva força a resolução dos N+1 durante o desenvolvimento.
# app/models/article.rb
# Enable strict loading by default on the model
class Article < ApplicationRecord
# Any non-preloaded association access raises an exception
self.strict_loading_by_default = true
belongs_to :author
has_many :comments
endO carregamento estrito também pode ser ativado para uma consulta específica.
# app/controllers/articles_controller.rb
# Strict loading on a specific query
class ArticlesController < ApplicationController
def index
# Raises StrictLoadingViolationError if a non-included
# association is accessed
@articles = Article.strict_loading.includes(:author)
end
endSelect e Pluck para dados parciais
Quando apenas algumas colunas são necessárias, select e pluck reduzem a quantidade de dados transferidos do banco de dados.
# app/controllers/reports_controller.rb
# Optimization with select and pluck
class ReportsController < ApplicationController
def titles_only
# select returns Article objects with only id and title
@articles = Article.select(:id, :title)
end
def title_array
# pluck returns an Array of values, not AR objects
# More performant when only values are needed
@titles = Article.pluck(:title)
# => ["First article", "Second article", ...]
end
endCounter Cache para contagens
As contagens de associações (article.comments.count) geram uma consulta SQL a cada chamada. O counter cache armazena essa contagem diretamente na tabela pai.
# app/models/comment.rb
# Counter cache configuration
class Comment < ApplicationRecord
# Rails automatically maintains the counter in articles.comments_count
belongs_to :article, counter_cache: true
endA migração adiciona a coluna de contagem com um valor padrão.
# db/migrate/20260223_add_comments_count_to_articles.rb
# Migration to add counter cache
class AddCommentsCountToArticles < ActiveRecord::Migration[7.1]
def change
add_column :articles, :comments_count, :integer, default: 0, null: false
# Initialize counters for existing data
Article.find_each do |article|
Article.reset_counters(article.id, :comments)
end
end
endApós essa configuração, article.comments_count lê a coluna diretamente, sem consultas SQL adicionais.
# app/views/articles/index.html.erb
# Using counter cache (no SQL query)
<% @articles.each do |article| %>
<p><%= article.title %> - <%= article.comments_count %> comments</p>
<% end %>Pronto para mandar bem nas entrevistas de Ruby on Rails?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Boas práticas e checklist
Uma abordagem sistemática previne os problemas N+1 em novos desenvolvimentos e corrige progressivamente o código existente.
Análise da view antes do código
Antes de escrever o código do controller, é recomendável analisar a view para identificar todas as associações acessadas. Essa antecipação evita esquecimentos.
# app/controllers/articles_controller.rb
# Pre-analyze view to identify required includes
class ArticlesController < ApplicationController
def show
# View accesses: author, author.company, comments, comments.user
# All these must be preloaded
@article = Article.includes(
author: :company,
comments: :user
).find(params[:id])
end
endScopes reutilizáveis
Centralizar os includes frequentes em scopes simplifica a manutenção e garante consistência.
# app/models/article.rb
# Reusable scopes for preloading
class Article < ApplicationRecord
# Scope for list display
scope :with_author, -> { includes(:author) }
# Scope for detailed display
scope :with_full_details, -> {
includes(
author: :company,
comments: { user: :avatar_attachment },
tags: []
)
}
# Scope for admin with all relations
scope :for_admin, -> {
includes(:author, :comments, :tags, :category)
.with_attached_cover_image
}
endChecklist de prevenção
Esta checklist resume os pontos essenciais de verificação para evitar problemas N+1:
- Instalar e configurar Bullet ou Prosopite em desenvolvimento
- Ativar Bullet.raise em CI para bloquear regressões
- Analisar as views para identificar associações antes de escrever os controllers
- Usar
includespor padrão,eager_loadse houver filtragem em associações - Criar scopes reutilizáveis para os padrões de pré-carregamento frequentes
- Utilizar strict loading em modelos sensíveis
- Adicionar counter caches para contagens frequentes
- Verificar regularmente os logs SQL em desenvolvimento
Pré-carregar associações em excesso consome memória desnecessariamente. Apenas as associações realmente utilizadas pela view devem ser pré-carregadas. Ferramentas como Bullet também detectam o "unused eager loading".
Conclusão
As consultas N+1 representam um problema de desempenho importante, facilmente prevenível em aplicações Rails. A combinação de ferramentas de detecção automatizadas e boas práticas de desenvolvimento elimina esse problema com eficácia.
Pontos-chave:
includesresolve a maioria dos casos N+1 ao pré-carregar as associaçõeseager_loadé necessário ao filtrar ou ordenar por associações- Bullet e Prosopite detectam automaticamente os problemas em desenvolvimento
- Strict loading previne os N+1 lançando exceções
- Counter caches otimizam as contagens frequentes
- Analisar as views antes de escrever os controllers evita esquecimentos
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
Artigos relacionados

Perguntas de entrevista Ruby on Rails: Top 25 em 2026
As 25 perguntas de entrevista Ruby on Rails mais cobradas. Arquitetura MVC, Active Record, migrations, testing RSpec, APIs REST com respostas detalhadas e exemplos de código.

Ruby on Rails 7: Hotwire e Turbo para Aplicacoes Reativas
Guia completo de Hotwire e Turbo no Rails 7. Aprenda a construir aplicacoes reativas sem escrever JavaScript com Turbo Drive, Frames e Streams.

Action Cable e WebSockets no Rails: Guia Completo para Entrevistas Técnicas
Domine Action Cable e WebSockets no Ruby on Rails para entrevistas tecnicas. Aprenda configuracao de conexoes, canais, Solid Cable, Turbo Streams, escalabilidade com Redis e estrategias de teste com exemplos praticos de codigo.