Find, Resolve, and Prevent N+1 Query Issues in Rails.

Find, Resolve, and Prevent N+1 Query Issues in Rails.

This blog is a part of our "Guide to Rails Performance." To gain a deeper understanding of techniques for optimizing Rails code, Please explore our earlier blogs of this series:

1. Difference between Includes and Joins in Ruby on Rails

Introduction:

When you're building a web application with Ruby on Rails, it's essential to make it fast and responsive. One common issue that can slow down your app is called "N+1 queries." Imagine you're trying to show a list of students and the colleges they attend. Without careful coding, this can lead to your app making too many trips to the database, making it slow for users.

Let's look at an example. Suppose you want to display the first ten students and their colleges. Here's what the code might look like:

students = Student.limit(10)

students.each do |student|
  puts "#{student.college.name} build number #{student.name}"
end

This code works, but it's not efficient. It sends multiple queries to the database:

One query to get the students. And then, for each student, another query to get their college.

Student Load (0.3ms) SELECT "students".* FROM "students" LIMIT ? [["LIMIT", 10]]
College Load (0.2ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
College Load (0.2ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
College Load (0.2ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
College Load (0.2ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
College Load (0.2ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
College Load (0.2ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
College Load (0.2ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
College Load (0.1ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
College Load (0.2ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
College Load (0.1ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]

Even for just ten students, this results in eleven separate database queries. In a real-world scenario, you might need to fetch thousands of records, and that can severely slow down your app.

Identifying N+1 Query Problems

The first step in addressing N+1 query issues is to identify them. Thankfully, Rails provides a powerful tool called the Bullet gem that can help you pinpoint these problems in your code.

Using the Bullet Gem To get started, add the Bullet gem to your Gemfile:

gem 'bullet'

Run bundle install to install the gem. Next, configure it in your development environment by adding the following to your config/environments/development.rb file:

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

With Bullet enabled, it will alert you to N+1 query issues when you run your Rails application in development mode. Keep an eye out for these alerts as you interact with your application.

Fixing N+1 Queries

Once you've identified N+1 query problems, it's essential to fix them. Here are some common techniques to address these issues:

Eager Loading

One of the most effective ways to resolve N+1 queries is by using eager loading. ActiveRecord provides methods like includes, joins, and preload to fetch associated records in a single query rather than issuing N separate queries. For example:

students = Student.includes(:college).limit(10)

students.each do |student|
  puts "#{student.college.name} build number #{student.name}"
end

This time we'll use one query to fetch the students and another for fetching the associated colleges.

Student Load (0.4ms) SELECT "students".* FROM "students"
College Load (0.4ms) SELECT "colleges".* FROM "colleges" WHERE "colleges"."id" IN (?, ?, ?, ?) [["id", 1], ["id", 2], ["id", 3], ["id", 4]]

Preventing N+1 Queries

While fixing N+1 queries is crucial, it's equally important to prevent them from happening in the first place. Here are some best practices for preventing N+1 query issues:

Use joins or includes.

Whenever you're searching for data and you know you'll also need related information, make sure to use "joins" or "includes" to retrieve all the needed data at once.For guidance on when to use "joins" or "includes," you can refer to this blog post: Difference between Includes and Joins in Ruby on Rails

By detecting, resolving, and proactively avoiding N+1 query concerns in your Rails application, you can boost its speed and create a more enjoyable experience for your users. If you found this blog helpful, please consider sharing it with others.