# ActiveRecord: Fixing N+1 Query Problems in Ruby on Rails > Complete guide to detecting and fixing N+1 query problems in Rails with ActiveRecord. Master includes, preload, eager_load and automated detection tools. - Published: 2026-02-23 - Updated: 2026-03-31 - Author: SharpSkill - Tags: ruby on rails, activerecord, performance, n+1 queries, sql optimization - Reading time: 10 min --- N+1 queries represent one of the most common performance problems in Rails applications. A simple loop over records can trigger hundreds of unnecessary SQL queries, drastically slowing down response times. This guide covers detection and resolution techniques to ensure performant Rails applications. > **Production Impact** > > A page displaying 50 articles with their authors can generate 51 SQL queries instead of just one. In production with thousands of users, this problem becomes critical for response times and server load. ## Understanding the N+1 Problem The N+1 problem occurs when code executes one query to retrieve a list of records (1 query), then runs an additional query for each record to access its associations (N queries). The name "N+1" describes exactly this pattern: 1 initial query + N queries for associations. Consider a concrete example with articles and their authors. Without optimization, each access to an article's author triggers a new SQL query. ```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 ``` In the view, each call to `article.author` triggers an additional query to the database. ```erb <% @articles.each do |article| %>

<%= article.title %>

By <%= article.author.name %>

<% end %> ``` For 100 articles, this code generates 101 SQL queries. Rails logs clearly show the problem with repetitive queries. ```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 ``` ## Solving with includes The `includes` method is the most common and recommended solution for fixing N+1 problems. It tells ActiveRecord to preload associations in one or two optimized queries. ```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 ``` With `includes`, ActiveRecord executes only two queries regardless of article count. The first retrieves all articles, the second retrieves all relevant authors. ```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, ...) ``` Nested associations can also be preloaded using hash syntax. This approach is essential when views access multiple levels of associations. ```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 ``` > **Golden Rule** > > If a view accesses an association inside a loop, that association must be preloaded in the controller with `includes`. Always verify association access patterns in views. ## Differences Between includes, preload, and eager_load Rails provides three methods for association preloading. Each uses a different SQL strategy, with specific use cases. ### preload: Separate Queries The `preload` method always executes separate queries for each association. It works efficiently when no WHERE conditions filter on associations. ```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 The `eager_load` method uses a LEFT OUTER JOIN to load data in a single query. It becomes mandatory when filtering or sorting on association columns. ```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: Smart Behavior The `includes` method automatically chooses the best strategy. It uses `preload` by default, but switches to `eager_load` if a WHERE clause references the association. ```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 ``` The following table summarizes differences between the three methods. | Method | SQL Strategy | Use Case | |--------|--------------|----------| | `preload` | Separate queries | Simple preloading, no filtering | | `eager_load` | LEFT OUTER JOIN | Filtering/sorting on associations | | `includes` | Automatic | General use, recommended default | ## Automated N+1 Detection Manual detection of N+1 problems is tedious and error-prone. Several tools automate this detection in development and CI. ### Bullet: Real-Time Detection The Bullet gem analyzes SQL queries in real-time and alerts on detected N+1 problems. It also suggests appropriate fixes. ```ruby # Gemfile # Bullet detects N+1 in development group :development do gem 'bullet' end ``` Configuration in the development environment enables various alert modes. ```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 ``` When an N+1 problem is detected, Bullet displays an explicit message with the recommended solution. ```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 in CI** > > In continuous integration, enabling `Bullet.raise = true` causes tests to fail if an N+1 problem is detected. This prevents performance regressions. ### Prosopite: Lightweight Alternative The Prosopite gem offers a lighter alternative to Bullet, with minimal configuration and test compatibility. ```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 ``` ## Advanced Optimization Techniques Beyond basic methods, several techniques allow fine-tuned ActiveRecord query optimization. ### Strict Loading: Default Prevention Rails 6.1+ provides strict loading mode that raises an exception if a non-preloaded association is accessed. This preventive approach forces N+1 resolution during development. ```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 ``` Strict loading can also be enabled on a specific query basis. ```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 and Pluck for Partial Data When only certain columns are needed, `select` and `pluck` reduce the amount of data transferred from the database. ```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 for Counts Association counts (`article.comments.count`) generate a SQL query on each call. Counter cache stores this count directly in the parent table. ```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 ``` The migration adds the count column with a default value. ```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 ``` After this configuration, `article.comments_count` reads the column directly without additional SQL queries. ```ruby # app/views/articles/index.html.erb # Using counter cache (no SQL query) <% @articles.each do |article| %>

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

<% end %> ``` ## Best Practices and Checklist A systematic approach prevents N+1 problems in new development and progressively fixes existing code. ### View Analysis Before Coding Before writing controller code, analyze the view to identify all accessed associations. This anticipation prevents omissions. ```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 ``` ### Reusable Scopes Centralizing frequent includes in scopes simplifies maintenance and ensures consistency. ```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 ``` ### Prevention Checklist This checklist summarizes essential verification points to avoid N+1 problems: - Install and configure Bullet or Prosopite in development - Enable Bullet.raise in CI to block regressions - Analyze views to identify associations before writing controllers - Use `includes` by default, `eager_load` if filtering on associations - Create reusable scopes for frequent preloading patterns - Use strict loading on sensitive models - Add counter caches for frequent counts - Regularly check SQL logs in development > **Over-Preloading Warning** > > Preloading too many associations consumes memory unnecessarily. Only preload what the view actually uses. Tools like Bullet also detect "unused eager loading". ## Conclusion N+1 queries represent a major performance problem that is easily preventable in Rails applications. A combination of automated detection tools and development best practices eliminates this issue effectively. **Key takeaways:** - `includes` solves most N+1 cases by preloading associations - `eager_load` is required when filtering or sorting on associations - Bullet and Prosopite automatically detect problems in development - Strict loading prevents N+1 by raising exceptions - Counter caches optimize frequent counts - Analyzing views before writing controllers prevents omissions --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/ruby-on-rails/activerecord-fixing-n-plus-1-query-problems