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.
Model.find_each instead of
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
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
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.