# 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.
- Published: 2026-02-23
- Updated: 2026-04-29
- Author: Anthony Fillion-Maillet
- Tags: ruby on rails, activerecord, performance, n+1 queries, sql optimization
- Reading time: 10 min
---
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.
> **Impacto em produção**
>
> 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.
```ruby
# 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
end
```
Na view, cada chamada a `article.author` dispara uma consulta adicional ao banco de dados.
```erb
<% @articles.each do |article| %>
<%= article.title %>
By <%= article.author.name %>
<% end %>
```
Para 100 artigos, esse código gera 101 consultas SQL. Os logs do Rails mostram claramente o problema com consultas repetitivas.
```sql
-- 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 queries
```
## Resolvendo 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.
```ruby
# 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
end
```
Com `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.
```sql
-- 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.
```ruby
# 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
end
```
> **Regra de ouro**
>
> Se 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.
```ruby
# 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.
```ruby
# 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" ASC
```
### includes: 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.
```ruby
# 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
end
```
A 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.
```ruby
# Gemfile
# Bullet detects N+1 in development
group :development do
gem 'bullet'
end
```
A configuração no ambiente de desenvolvimento permite ativar diversos modos de alerta.
```ruby
# 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
end
```
Quando um problema N+1 é detectado, o Bullet exibe uma mensagem explícita com a solução recomendada.
```text
# 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:5
```
> **Bullet em CI**
>
> Na 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.
```ruby
# Gemfile
# Prosopite as an alternative to Bullet
group :development, :test do
gem 'prosopite'
end
```
```ruby
# config/environments/development.rb
# Prosopite configuration
Rails.application.configure do
config.after_initialize do
Prosopite.rails_logger = true
Prosopite.raise = Rails.env.test?
end
end
```
## Té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.
```ruby
# 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
end
```
O carregamento estrito também pode ser ativado para uma consulta específica.
```ruby
# 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
end
```
### Select 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.
```ruby
# 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
end
```
### Counter 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.
```ruby
# 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
end
```
A migração adiciona a coluna de contagem com um valor padrão.
```ruby
# 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
end
```
Após essa configuração, `article.comments_count` lê a coluna diretamente, sem consultas SQL adicionais.
```ruby
# app/views/articles/index.html.erb
# Using counter cache (no SQL query)
<% @articles.each do |article| %>