Performance issues in Rails apps can sneak up on you, especially if you're working with ActiveRecord and are unaware of the infamous N+1 query problem. This post will walk you through what N+1 queries are, how to detect them, and how to fix them using simple techniques. If you're a beginner or intermediate Rails developer, this is one of the easiest ways to level up your performance game.
What is an N+1 Query?
Imagine going to the store with a shopping list of 10 items and making a separate trip for each one instead of buying them all at once. That’s what an N+1 query does: it performs 1 query to fetch a list of records (say Post.all
), and then 1 additional query for each associated record (like post.comments
).
# N+1 problem example
Post.all.each do |post|
puts post.comments.count
end
This triggers one query to get all posts, and then one query per post to fetch its comments. If you have 10 posts, that’s 11 queries. Scale that up to 100 posts, and you're dealing with 101 queries, most of them unnecessary.
How to Detect N+1 Queries
1. Check Your Logs
In development, check your log/development.log
or Rails server output. You'll often see the same query being run repeatedly.
2. Use the bullet
Gem
The bullet gem is designed to catch N+1 queries and notify you.
Setup:
# Gemfile
group :development do
gem 'bullet'
end
Configuration:
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.rails_logger = true
end
You'll get an alert in your browser or console when Bullet detects a potential N+1.
How to Fix N+1 Queries
Use Eager Loading with .includes
Eager loading tells Rails to fetch associated records in the same query using LEFT OUTER JOIN
.
# Fixing the N+1 problem
Post.includes(:comments).each do |post|
puts post.comments.count
end
Now, instead of 11 queries for 10 posts, you only make 2: one for the posts, and one for the associated comments.
When to Use .joins
or .preload
- Use
.joins
when you're filtering or ordering based on associated data. - Use
.preload
when you want to load associations but don’t need joins.
Common Mistakes and Gotchas
- Forgetting to use eager loading in controllers or views.
- Not eager loading nested associations (
post.comments.user
requiresPost.includes(comments: :user)
). - Overusing
.includes
can also hurt performance if you load too much data.
Real-World Example
Say you're building a blog app and rendering a list of posts with their comments:
<% @posts.each do |post| %>
<h2><%= post.title %></h2>
<% post.comments.each do |comment| %>
<p><%= comment.body %></p>
<% end %>
<% end %>
If @posts = Post.all
, you’re triggering N+1 queries. Instead, use:
@posts = Post.includes(:comments)
This change alone can dramatically cut down your query count and improve performance.
Preventing N+1s Going Forward
- Use the
bullet
gem in development to get real-time alerts. - Build a habit of checking logs when building or refactoring features.
- Write performance tests if your app grows in size or complexity.
Want to keep improving your Rails performance? There's a performance chapter in my Build A SaaS App in Ruby on Rails 8 book that covers this and more.
Top comments (5)
yeah this hits home, caught myself messing this up so many times - you think habits are enough to spot these, or we always stuck needing tools like bullet?
Yeah, it definitely gets built into muscle memory when you reach into associations in views, etc, but tools like Bullet keep us from making our mistakes
This brings back memories of spending hours debugging slow pages, only to find an N+1 lurking. Wild how much smoother things get with just a single .includes!
pretty cool breakdown - stuff like this makes rails less scary imo. you ever feel like most folks ignore performance until its way too late
Sometimes it isn't always the top priority to optimize when you're building (or at least be in the mindset). However, if you know you'll be referencing associations in views/JSON it doesn't hurt to add the
.includes
up frontSome comments may only be visible to logged-in visitors. Sign in to view all comments.