Tips for Writing Fast Rails: Part 3

Here we continue with the series of articles where we talk about how minor adjustments in the code can lead to major performance improvements.

In this article we'll focus on the use of ActiveRecord::Batches#find_each when it comes to iterations across a large number of records.

Prefer Model.find_each instead of Model.all.each

A very common mistake (often seen in background jobs) is to use ActiveRecord::Scoping::Named::ClassMethods#all + ActiveRecord::Result#each (Model.all.each) to iterate a table with several thousands of records on it.

Product.all.each do |product|
  product.update_column(:stock, 50)
end

The problem with that code is that Product.all initializes all the records at once and loads them into memory. This can lead to major memory usage issues in your app.

To be clear, the problem there is not the .all but the amount of records that it loads. If you try to iterate a scope like Product.where(status: :active) that still loads a huge amount of records, it will lead to the same issues.

To solve this issue we should instead load the records in batches (1000 by default) using find_each. This will significantly reduce memory consumption.

Product.find_each do |product|
  product.update_column(:stock, 50)
end

To set a custom batch size you can use the batch_size option:

Product.find_each(batch_size: 200) do |product|
  product.update_column(:stock, 50)
end

You can even specify the starting point for the batch, which is especially useful if you want multiple workers dealing with the same processing queue. You can make one worker handle all the records between ID 1 and 5000 and another handle from ID 5000 and beyond.

Product.find_each(batch_size: 200, start: 5000) do |product|
  product.update_column(:stock, 50)
end

Conclusion

As long as the objects are not loaded into memory yet, it is a good practice to always use find_each, even when there are not many records on the table. That way if the table does grow significantly, you don't have to worry about this issue in the future.

If you want to detect other performance issues that you may have in your app, consider using the performance extension for Rubocop.

Finally, if you need help improving the performance of your Rails application, get in touch with us! We are constantly looking for new projects and opportunities to help improve the performance of Rails apps.