Avoiding N+1 Queries in Rails: Easy Performance Wins for Beginners
Performance issues in Rails apps can sneak up on you, especially if you’re working with ActiveRecord and 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 examplePost.all.each do |post| puts post.comments.countend
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:
# Gemfilegroup :development do gem 'bullet'end
Configuration:
config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.rails_logger = trueend
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 problemPost.includes(:comments).each do |post| puts post.comments.countend
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.