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.