Skip to content

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 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 requires Post.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.