ActiveRecord: Resolver problemas de consultas N+1 en Ruby on Rails
Guía completa para detectar y resolver consultas N+1 en Rails con ActiveRecord. Domina includes, preload, eager_load y herramientas de detección automática.

Las consultas N+1 representan uno de los problemas de rendimiento más comunes en aplicaciones Rails. Un simple bucle sobre registros puede desencadenar cientos de consultas SQL innecesarias, ralentizando drásticamente los tiempos de respuesta. Esta guía cubre las técnicas de detección y resolución para garantizar aplicaciones Rails performantes.
Una página que muestra 50 artículos con sus autores puede generar 51 consultas SQL en lugar de una sola. En producción con miles de usuarios, este problema se vuelve crítico para los tiempos de respuesta y la carga del servidor.
Comprender el problema N+1
El problema N+1 ocurre cuando el código ejecuta una consulta para obtener una lista de registros (1 consulta), y luego ejecuta una consulta adicional por cada registro para acceder a sus asociaciones (N consultas). El nombre "N+1" describe exactamente este patrón: 1 consulta inicial + N consultas para las asociaciones.
Un ejemplo concreto con artículos y sus autores ilustra el problema. Sin optimización, cada acceso al autor de un artículo desencadena una nueva 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
endEn la vista, cada llamada a article.author desencadena una consulta adicional a la base de datos.
<!-- 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 artículos, este código genera 101 consultas SQL. Los logs de Rails muestran claramente el problema con 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 queriesResolver con includes
El método includes es la solución más común y recomendada para corregir los problemas N+1. Indica a ActiveRecord que precargue las asociaciones en una o dos consultas optimizadas.
# 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
endCon includes, ActiveRecord ejecuta solo dos consultas independientemente del número de artículos. La primera obtiene todos los artículos, la segunda recupera todos los 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, ...)Las asociaciones anidadas también pueden precargarse usando la sintaxis de hash. Este enfoque resulta esencial cuando las vistas acceden a varios niveles de asociaciones.
# 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
endSi una vista accede a una asociación dentro de un bucle, esa asociación debe precargarse en el controlador con includes. Verificar siempre los patrones de acceso a asociaciones en las vistas.
Diferencias entre includes, preload y eager_load
Rails ofrece tres métodos para la precarga de asociaciones. Cada uno utiliza una estrategia SQL diferente, con casos de uso específicos.
preload: consultas separadas
El método preload siempre ejecuta consultas separadas para cada asociación. Funciona eficientemente cuando ninguna condición WHERE filtra sobre las asociaciones.
# 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
El método eager_load utiliza un LEFT OUTER JOIN para cargar los datos en una sola consulta. Se vuelve obligatorio al filtrar u ordenar sobre columnas de asociaciones.
# 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: comportamiento inteligente
El método includes elige automáticamente la mejor estrategia. Utiliza preload por defecto, pero cambia a eager_load si una cláusula WHERE hace referencia a la asociación.
# 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
endLa siguiente tabla resume las diferencias entre los tres métodos.
| Método | Estrategia SQL | Caso de uso |
|--------|----------------|-------------|
| preload | Consultas separadas | Precarga simple, sin filtrado |
| eager_load | LEFT OUTER JOIN | Filtrado/ordenación sobre asociaciones |
| includes | Automático | Uso general, valor predeterminado recomendado |
Detección automatizada de N+1
La detección manual de problemas N+1 resulta tediosa y propensa a errores. Varias herramientas automatizan esta detección en desarrollo y CI.
Bullet: detección en tiempo real
La gema Bullet analiza las consultas SQL en tiempo real y alerta sobre los problemas N+1 detectados. También sugiere las correcciones apropiadas.
# Gemfile
# Bullet detects N+1 in development
group :development do
gem 'bullet'
endLa configuración en el entorno de desarrollo permite activar varios 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
endCuando se detecta un problema N+1, Bullet muestra un mensaje explícito con la solución 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:5En integración continua, activar Bullet.raise = true provoca que las pruebas fallen si se detecta un problema N+1. Esto previene regresiones de rendimiento.
Prosopite: alternativa ligera
La gema Prosopite ofrece una alternativa más ligera a Bullet, con una configuración mínima y compatibilidad con las pruebas.
# 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 avanzadas de optimización
Más allá de los métodos básicos, varias técnicas permiten afinar la optimización de las consultas ActiveRecord.
Strict Loading: prevención por defecto
Rails 6.1+ proporciona un modo de carga estricta que lanza una excepción si se accede a una asociación no precargada. Este enfoque preventivo obliga a resolver los N+1 durante el desarrollo.
# 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
endLa carga estricta también puede activarse para una 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 y Pluck para datos parciales
Cuando solo se necesitan ciertas columnas, select y pluck reducen la cantidad de datos transferidos desde la base de datos.
# 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 conteos
Los conteos de asociaciones (article.comments.count) generan una consulta SQL en cada llamada. Counter cache almacena este conteo directamente en la tabla padre.
# 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
endLa migración añade la columna de conteo con un valor predeterminado.
# 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
endDespués de esta configuración, article.comments_count lee directamente la columna sin consultas SQL adicionales.
# 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 %>¿Listo para aprobar tus entrevistas de Ruby on Rails?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Buenas prácticas y checklist
Un enfoque sistemático previene los problemas N+1 en los nuevos desarrollos y corrige progresivamente el código existente.
Análisis de la vista antes del código
Antes de escribir el código del controlador, conviene analizar la vista para identificar todas las asociaciones accedidas. Esta anticipación evita olvidos.
# 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 reutilizables
Centralizar los includes frecuentes en scopes simplifica el mantenimiento y garantiza la coherencia.
# 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 prevención
Esta checklist resume los puntos esenciales de verificación para evitar los problemas N+1:
- Instalar y configurar Bullet o Prosopite en desarrollo
- Activar Bullet.raise en CI para bloquear regresiones
- Analizar las vistas para identificar las asociaciones antes de escribir los controladores
- Usar
includespor defecto,eager_loadsi hay filtrado sobre asociaciones - Crear scopes reutilizables para los patrones de precarga frecuentes
- Utilizar strict loading sobre los modelos sensibles
- Añadir counter caches para los conteos frecuentes
- Verificar regularmente los logs SQL en desarrollo
Precargar demasiadas asociaciones consume memoria innecesariamente. Solo deben precargarse las asociaciones que la vista realmente utilice. Herramientas como Bullet también detectan el "unused eager loading".
Conclusión
Las consultas N+1 representan un problema mayor de rendimiento, fácilmente prevenible en aplicaciones Rails. La combinación de herramientas de detección automatizadas y buenas prácticas de desarrollo elimina este problema de forma efectiva.
Puntos clave:
includesresuelve la mayoría de los casos N+1 al precargar las asociacioneseager_loades necesario al filtrar u ordenar sobre las asociaciones- Bullet y Prosopite detectan automáticamente los problemas en desarrollo
- Strict loading previene los N+1 lanzando excepciones
- Counter caches optimizan los conteos frecuentes
- Analizar las vistas antes de escribir los controladores evita olvidos
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Etiquetas
Compartir
Artículos relacionados

Preguntas de entrevista Ruby on Rails: Top 25 en 2026
Las 25 preguntas de entrevista Ruby on Rails más solicitadas. Arquitectura MVC, Active Record, migraciones, testing RSpec, APIs REST con respuestas detalladas y ejemplos de código.

Ruby on Rails 7: Hotwire y Turbo para Aplicaciones Reactivas
Guia completa de Hotwire y Turbo en Rails 7. Aprende a construir aplicaciones reactivas sin escribir JavaScript con Turbo Drive, Frames y Streams.

Action Cable y WebSockets en Rails: Guía Completa para Entrevistas Técnicas
Guia completa sobre Action Cable y WebSockets en Ruby on Rails para entrevistas tecnicas. Conexiones, canales, Solid Cable, Turbo Streams, escalabilidad con Redis y testing con ejemplos de codigo.