# 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. - 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 --- 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. > **Impacto en producción** > > 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. ```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 ``` En la vista, cada llamada a `article.author` desencadena una consulta adicional a la base de datos. ```erb <% @articles.each do |article| %>

<%= article.title %>

By <%= article.author.name %>

<% end %> ``` Para 100 artículos, este código genera 101 consultas SQL. Los logs de Rails muestran claramente el problema con 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 ``` ## Resolver 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. ```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 ``` Con `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. ```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, ...) ``` 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. ```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 ``` > **Regla de oro** > > Si 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. ```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 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. ```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: 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. ```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 ``` La 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. ```ruby # Gemfile # Bullet detects N+1 in development group :development do gem 'bullet' end ``` La configuración en el entorno de desarrollo permite activar varios 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 ``` Cuando se detecta un problema N+1, Bullet muestra un mensaje explícito con la solución 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 en CI** > > En 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. ```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 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. ```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 ``` La carga estricta también puede activarse para una 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 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. ```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 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. ```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 ``` La migración añade la columna de conteo con un valor predeterminado. ```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 ``` Después de esta configuración, `article.comments_count` lee directamente la columna sin consultas SQL adicionales. ```ruby # app/views/articles/index.html.erb # Using counter cache (no SQL query) <% @articles.each do |article| %>

<%= article.title %> - <%= article.comments_count %> comments

<% end %> ``` ## 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. ```ruby # 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 end ``` ### Scopes reutilizables Centralizar los includes frecuentes en scopes simplifica el mantenimiento y garantiza la coherencia. ```ruby # 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 } end ``` ### Checklist 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 `includes` por defecto, `eager_load` si 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 > **Atención al exceso de precarga** > > 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:** - `includes` resuelve la mayoría de los casos N+1 al precargar las asociaciones - `eager_load` es 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/ruby-on-rails/activerecord-fixing-n-plus-1-query-problems