Tips for Writing Fast Rails: Part 1

Tips for Writing Fast Rails: Part 1

Rails opens a new window is a powerful framework. You can write a lot of features in a short period of time. In the process you can easily write code that performs poorly.

At OmbuLabs opens a new window we like to maintain Ruby on Rails applications opens a new window . In the process of maintaining them, adding features and fixing bugs, we like to improve the code and its performance (because we are good boy scouts opens a new window !)

Here are some tips based on our experience.

Prefer where instead of select

When you are performing a lot of calculations, you should load as little as possible into memory. Always prefer a SQL query vs. an object’s method call.

With ActiveRecord opens a new window , it’s easy to forget which methods load ActiveRecord::Base opens a new window objects into memory and which perform a simple query instead.

The bigger the table, the slower the object load. If you have a table with 80 columns (sigh!), loading each record will take a lot longer than a table with 3 columns. So, you must avoid object loads as much as possible. Only load objects into memory when you really need them.

For example:

shop_ids.map do |shop_id|
  products.select { |p| p.shop_id == shop_id }.first
end

select will load all the products into memory and then compare the ids. This will be slower than just using where.

This will be much faster:

shop_ids.map do |shop_id|
  products.where(shop_id: shop_id).first
end

Because it will perform the query and only after the query returns it will load the objects into memory.

Prefer pluck instead of map

If you are interested in only a few values per row, you should use pluck instead of map.

For example:

Order.where(number: 'R545612547').map &:id
# Order Load (5.0ms)  SELECT `orders`.* FROM `orders` WHERE `orders`.`number` = 'R545612547' ORDER BY orders.created_at DESC
=> [1]

As with select, map will load the order into memory and it will get the id attribute.

Using pluck will be faster, because it doesn’t need to load an entire object into memory.

So this will be much faster:

Order.where(number: 'R545612547').pluck :id
# SQL (0.8ms)  SELECT `orders`.`id` FROM `orders` WHERE `orders`.`number` = 'R545612547' ORDER BY orders.created_at DESC
=> [1]

For this particular case, pluck is six times faster than map.

Avoid N+1 Queries

There are some rare cases where you want an N+1 query in your application. For instance, when you are using a Russian Doll Caching opens a new window strategy, it’s a good idea. (full explanation in this interview with DHH opens a new window : https://youtu.be/ktZLpjCanvg?t=4m27s opens a new window )

If you are not using this caching strategy, you should get rid of all your N+1 query problems opens a new window by including the tables that you need before running the query.

For example:

Order.where("created_at > ?", 1.hour.ago)
     .find_each do |order|
  puts order.ship_address.try(:firstname)
end
  Order Load (7866.0ms)  SELECT `orders`.* FROM `orders` WHERE (created_at > '2016-10-05 18:05:48') ORDER BY `orders`.`id` ASC LIMIT 1000
  Address::ShipAddress Load (0.5ms)  SELECT `addresses`.* FROM `addresses` WHERE `addresses`.`type` IN ('Address::ShipAddress') AND `addresses`.`order_id` = 2619178 LIMIT 1
  Address::ShipAddress Load (0.5ms)  SELECT `addresses`.* FROM `addresses` WHERE `addresses`.`type` IN ('Address::ShipAddress') AND `addresses`.`order_id` = 2619179 LIMIT 1
  Address::ShipAddress Load (0.5ms)  SELECT `addresses`.* FROM `addresses` WHERE `addresses`.`type` IN ('Address::ShipAddress') AND `addresses`.`order_id` = 2619180 LIMIT 1
  Address::ShipAddress Load (0.5ms)  SELECT `addresses`.* FROM `addresses` WHERE `addresses`.`type` IN ('Address::ShipAddress') AND `addresses`.`order_id` = 2619181 LIMIT 1
  Address::ShipAddress Load (0.5ms)  SELECT `addresses`.* FROM `addresses` WHERE `addresses`.`type` IN ('Address::ShipAddress') AND `addresses`.`order_id` = 2619182 LIMIT 1
  # ... to N

This code will perform one query on the orders table and N queries on the addresses table.

This will be faster:

Order.eager_load(:ship_address)
     .where("orders.created_at > ?", 1.hour.ago)
     .find_each do |order|
  puts order.ship_address.try(:firstname)
end

This code will perform only one query. eager_load will perform a query with a LEFT OUTER JOIN opens a new window with the associated table (addresses).

If you use Order.includes(:ship_address) it will perform two queries one for the orders table and another one for the addresses table. To understand the difference between includes and eager_load, read this article about Rails 4 preloading opens a new window .

A good way to find N+1 queries is using bullet opens a new window to get warnings as you develop your application.

Conclusion

Sometimes it takes only a few lines of code to improve the performance of your Rails opens a new window application. Before you start refactoring your code to perform faster, you should make sure that you have coverage for the methods that you’re improving.

If you found this article interesting, check out Erik Michaels-Ober’s talk about Writing Fast Ruby opens a new window : https://www.youtube.com/watch?v=fGFM_UrSp70 opens a new window . It has great tips for improving performance in your Ruby application or library.

And, if you need help improving the performance opens a new window of your Rails application, get in touch opens a new window ! We are constantly looking for new projects and opportunities to improve your Rails opens a new window performance.

Get the book