Finding N+1 Queries in Rails
N+1 queries are easy to miss because the page still works.
The first version ships. The table has ten rows. Then production gets real data, and one request starts doing hundreds of small queries. This is the Rails performance issue I check before reaching for more complex fixes.
The shape
This looks harmless:
posts = Post.all
posts.each do |post|
puts post.author.name
end But Rails loads author lazily:
SELECT * FROM posts;
SELECT * FROM authors WHERE id = 1;
SELECT * FROM authors WHERE id = 2;
SELECT * FROM authors WHERE id = 3; One query for the list, then one query per row.
Start with Bullet
In development, I still like bullet because it shows the problem while the page is open:
# Gemfile
gem 'bullet', group: [:development, :test] # config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.rails_logger = true
Bullet.console = true
end When it finds a problem, the message is usually direct:
N+1 Query detected
Post => [:author]
Add to your query: .includes(:author) I still check the Rails log too. Repeated SQL with only the id changing is the giveaway.
Load the relation up front
Most of the time, includes is enough:
posts = Post.includes(:author).where(published: true)
posts.each do |post|
puts post.author.name
end Rails now loads posts and authors in batches:
SELECT * FROM posts WHERE published = true;
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5); Nested relationships work the same way:
Post.includes(author: :profile, comments: :user) When includes is not what I want
I keep these three in mind:
| Method | What it does | When I use it |
|---|---|---|
includes | Lets Rails choose | Default option |
preload | Separate queries | I do not need SQL conditions on the join |
eager_load | LEFT OUTER JOIN | I need to filter or order by the association |
Example:
Post.eager_load(:author).where(authors: { active: true }) Places I check first
The N+1 often hides outside the controller.
Serializers:
class PostSerializer
def author_name
object.author.name
end
end Views:
<% @posts.each do |post| %>
<%= post.author.avatar_url %>
<% end %> Counts:
post.comments.count For counts, I usually add a counter cache:
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
end Then read:
post.comments_count A stricter option
For newer Rails apps, strict loading is useful in development:
config.active_record.strict_loading_by_default = true Or per query:
Post.strict_loading.includes(:author) Lazy loading then raises instead of quietly adding another query.
How I check it
When a Rails page feels slow, I check this order:
- Open the page with Bullet enabled.
- Scan the Rails log for repeated queries.
- Add
includes,preload, oreager_load. - Check serializers and views.
- Use counter caches for repeated counts.
Most slow Rails pages get easier to reason about after this pass.