Back

Eliminating N+1 Queries in Ruby on Rails: A Complete Guide

Learn how to identify, fix, and prevent N+1 queries in Rails applications with practical examples from production systems

January 20, 2024Ruby on RailsDatabasePerformance

Eliminating N+1 Queries in Ruby on Rails: A Complete Guide

N+1 queries are one of the most common performance bottlenecks in Rails applications. Learn how to identify, fix, and prevent them with practical examples from production systems.

What Are N+1 Queries?

N+1 queries occur when your application makes one query to fetch a collection of records, and then makes an additional query for each record in that collection. For example, fetching 100 users and then making 100 separate queries to get each user's posts.

Identifying N+1 Queries

Using Bullet Gem

The Bullet gem is an excellent tool for detecting N+1 queries in development:

# Gemfile gem 'bullet', group: 'development' # config/environments/development.rb config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.console = true end

Reading Rails Logs

Look for patterns like this in your logs:

User Load (0.5ms)  SELECT "users".* FROM "users"
Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]
Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 3]]

Solutions

1. Eager Loading with includes

The most common solution is to use includes to load associations:

# Bad - N+1 query users = User.all users.each do |user| puts user.posts.count end # Good - Eager loading users = User.includes(:posts).all users.each do |user| puts user.posts.count end

2. Using preload

When you know you'll need the associations but don't need to query on them:

users = User.preload(:posts, :comments).all

3. Using eager_load

For LEFT OUTER JOIN queries:

users = User.eager_load(:posts).where(posts: { published: true })

4. Counter Caches

For simple counts, use counter caches:

class Post < ApplicationRecord belongs_to :user, counter_cache: true end # Migration add_column :users, :posts_count, :integer, default: 0

Real-World Example

Here's a real optimization from a production system:

# Before - 1 + 100 queries def index @users = User.all # In view: user.posts.count causes N+1 end # After - 2 queries def index @users = User.includes(:posts).all end

Nested Associations

For deeply nested associations:

# Load users with posts and their comments users = User.includes(posts: :comments).all # Load users with multiple associations users = User.includes(:posts, :profile, comments: :likes).all

Best Practices

  • Always use Bullet in development to catch N+1 queries early
  • Test with production-like data to identify performance issues
  • Profile your queries using tools like rack-mini-profiler
  • Use counter caches for simple counts
  • Add database indexes on foreign keys
  • Monitor query performance in production

Conclusion

N+1 queries can severely impact application performance. By using eager loading, counter caches, and monitoring tools like Bullet, you can significantly improve your Rails application's performance and provide a better user experience.

Remember: always profile your queries and test with realistic data volumes to catch these issues before they reach production.

Enjoyed this article?

Share it with others who might find it useful.