DEV Community

Rob Race
Rob Race

Posted on • Originally published at robrace.dev

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 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Configuration:

# config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.rails_logger = true
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

If @posts = Post.all, you’re triggering N+1 queries. Instead, use:

@posts = Post.includes(:comments)
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
nevodavid profile image
Nevo David

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?

Collapse
 
rob__race profile image
Rob Race

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

Collapse
 
dotallio profile image
Dotallio

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!

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

pretty cool breakdown - stuff like this makes rails less scary imo. you ever feel like most folks ignore performance until its way too late

Collapse
 
rob__race profile image
Rob Race

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 front

Some comments may only be visible to logged-in visitors. Sign in to view all comments.