Introduction

One of the most common performance bottlenecks in Ruby on Rails applications is the N+1 query problem. This issue occurs when Rails executes multiple unnecessary database queries instead of fetching related records efficiently.

If left unchecked, N+1 queries can slow down page loads, increase database load, and degrade user experience. In this guide, we’ll cover:

✔️ How to identify N+1 query problems in Rails
✔️ Tools like Bullet to detect inefficient queries
✔️ Using includes, joins, and preload for optimization
✔️ Real-world examples of fixing N+1 queries


1. What is the N+1 Query Problem?

The N+1 query problem arises when Rails loads each associated record with a separate query instead of fetching all records in one efficient query.

Example of an N+1 Query Issue

Consider a User model that has_many :posts:

class User < ApplicationRecord  
has_many :posts  
end

class Post < ApplicationRecord  
belongs_to :user  
end  

Now, if we fetch users and their posts:

users = User.all  
users.each do |user|  
puts user.posts.count  
end  

🔴 What Happens?
1️⃣ First query: SELECT * FROM users; (Loads all users)
2️⃣ N additional queries: SELECT * FROM posts WHERE user_id = ?; (One query per user)

🚨 Performance Impact: If there are 100 users, Rails will execute 101 queries instead of 2.


2. Detecting N+1 Queries in Rails

There are multiple ways to detect N+1 queries before they impact performance.

Using Rails Logs

Set verbose query logs in config/environments/development.rb:

config.active_record.verbose_query_logs = true  

When running your app, check logs for repetitive queries like:

SELECT * FROM users;  
SELECT * FROM posts WHERE user_id = 1;  
SELECT * FROM posts WHERE user_id = 2;  
SELECT * FROM posts WHERE user_id = 3;  
...  
Using the Bullet Gem

The Bullet gem helps automatically detect N+1 queries in development.

1️⃣ Add Bullet to your Gemfile:

gem install bullet  

2️⃣ Enable Bullet in config/environments/development.rb:

config.after_initialize do  
Bullet.enable = true  
Bullet.alert = true  
Bullet.bullet_logger = true  
Bullet.console = true  
end  

3️⃣ Run your application and watch browser console alerts or logs for N+1 warnings.


3. Fixing N+1 Queries with Eager Loading

Rails provides three eager loading methods to fix N+1 queries:

Method Use Case Example Query
includes Preloads associations but may cause separate queries User.includes(:posts).all
preload Always loads associations in a separate query User.preload(:posts).all
joins Uses SQL JOIN, does not cache records User.joins(:posts).all
users = User.includes(:posts).all  
users.each do |user|  
puts user.posts.count  
end  

✔️ Fixes N+1 queries by loading all posts in one query:

SELECT * FROM users;  
SELECT * FROM posts WHERE user_id IN (1,2,3,...);  
Using joins for Filtering
users_with_posts = User.joins(:posts).where(posts: { published: true })  

✔️ Uses an INNER JOIN, fetching only users with published posts.


4. Optimizing Queries for Large Datasets

For large datasets, additional optimizations prevent memory issues and slow queries.

Paginate Large Queries
users = User.includes(:posts).limit(50).offset(100)  

✔️ Prevents loading thousands of records into memory.

Using Selective Loading

Instead of loading all columns, fetch only needed fields:

users = User.select(:id, :name).includes(:posts)  

✔️ Reduces database load by avoiding unnecessary data retrieval.


5. Real-World Performance Improvements

A real-world example of fixing N+1 queries in a Rails API:

🔴 Before Optimization:

def index  
render json: User.all.map { |user| { name: user.name, posts: user.posts.count } }  
end  

🚨 Issue: 100 users → 101 SQL queries

🟢 After Optimization:

def index  
users = User.includes(:posts).all  
render json: users.map { |user| { name: user.name, posts: user.posts.size } }  
end  

✔️ Optimized to just 2 SQL queries


6. Testing for N+1 Query Problems

Ensure your code remains optimized by writing tests.

Using RSpec with Bullet
require "rails_helper"

RSpec.describe "N+1 Queries", type: :request do  
it "avoids N+1 queries for users with posts" do  
Bullet.start_request  
get users_path  
expect(Bullet.notification?).to be_falsey  
Bullet.end_request  
end  
end  

✔️ Prevents accidental reintroduction of N+1 issues.


Conclusion

The N+1 query problem is one of the most common Rails performance pitfalls. However, with proper debugging and optimization techniques, applications can run efficiently without unnecessary database load.

🚀 Next Steps:
🔹 Use includes for eager loading associations
🔹 Install Bullet to detect N+1 issues in development
🔹 Optimize large datasets with pagination and selective loading
🔹 Write tests to prevent regression