Speeding up Rails Applications with Optimized Database Queries

This article will discuss the most basic technique to speed up a Rails application by optimizing database queries.

First, create an application.

1
2
rails new test_app
rails db:create db:migrate

Now create some models.

1
2
3
rails generate model User name
rails generate model Booking date:date user:references
rails db:migrate

Now add the following line in app/models/user.rb:

1
has_many :bookings

Add a few records in the database.

1
2
3
4
5
6
7
8
9
user = User.create(name: "John")
user.bookings.create(date: Date.today)
user.bookings.create(date: Date.today + 1)
user.bookings.create(date: Date.today + 2)

user2 = User.create(name: "Mike")
user2.bookings.create(date: Date.today)
user2.bookings.create(date: Date.today + 1)
user2.bookings.create(date: Date.today + 2)

Here’s how to get the user and iterate through its bookings in order to perform some actions.

1
2
3
4
bookings = Booking.all
bookings.each do |booking|
  puts booking.user.name
end

The above method will fire n + 1 queries, i.e. 6 + 1 = 7. This is because it will load all the bookings with line number one, and then for every booking it will hit the database to load its user. We have 6 bookings in total, so 7 queries in total.

These are the queries on the database:

1
2
3
4
5
6
7
Booking Load (0.3ms)  SELECT "bookings".* FROM "bookings"
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]

This n + 1 queries issue can be dealt with by eager loading. ActiveRecord provides an includes method that loads associated records while retrieving the actual record. Here’s how it works:

1
2
3
4
bookings = Booking.includes(:user)
bookings.each do |booking|
  puts booking.user.name
end

With the includes method, it will not hit only two queries as follows:

1
2
Booking Load (4.2ms)  SELECT "bookings".* FROM "bookings"
User Load (0.7ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2)  [["id", 1], ["id", 2]]

Reducing the number of queries from 7 to 2 may be no big deal, but imagine you have hundreds of associated records. In those cases, this scale of query reduction can make a huge difference.