Ruby on Rails Optimization Part 1
Overview
Hello there! If you're diving into this article, I'm guessing you have a thing for Ruby on Rails (RoR). If not, then perhaps curiosity has led you here—welcome either way! 😄
RoR, known for its elegant syntax, vibrant community, and the almost magical metaprogramming capabilities, truly makes coding an enjoyable experience. The syntactic sugar and the "magic" one-liners can often feel like a programming ballet. However, it's crucial to remember that every dance comes with its own set of moves that, if not executed correctly, can lead to performance pitfalls
Well I have some bad news, the syntactic sugar comes at a cost of performance and bad practices, so in this brief article we will go over some of the most common mistakes we do in our everyday coding and how to fix them.
A big shoutout to Abdourakhmane, whose wisdom and humility have been a beacon of learning. 🙏
1-The Misconception Around pluck
While it's often suggested that pluck
should be avoided in favor of select
to prevent database calls, the actual advice should be more nuanced. pluck
directly converts a database query result into an array of values for specified columns, which can be more efficient than loading ActiveRecord objects when you only need one or a few columns. The real optimization is understanding when to use pluck
versus select
. Use pluck
for retrieving values of a single column directly without the overhead of building ActiveRecord objects. Use select
when you need to utilize the returned objects in Rails, especially if you're chaining more queries onto the result.
# Use when you need an array of values from one column
User.pluck(:id)
# Use when you need ActiveRecord objects to chain more queries
User.select(:id, :name)
ActiveRecord queries are lazily loaded. This means the query is not executed until the data is actually needed. This can be both an advantage and a disadvantage, depending on how the query is used in the view.
Consider a model method that's used in a controller and then viewed in the view. If this method returns an ActiveRecord relation, the database query is only executed when the data is actually accessed in the view:
In the model :
class User < ApplicationRecord
def published_articles
articles.where(published: true).select(:title, :content)
# This does not execute the query
end
def bad_published_articles
articles.where(published: true).pluck(:title, :content)
# This executes the query
end
end
In the controller :
class UserController < ApplicationController
def show
@user = User.find(params[:id])
@articles = @user.published_articles
# This does not execute the query
end
end
In the view :
<% @articles.each do |article| %> <!-- The query executes here -->
<%= article.title %>
<% end %>
The pluck
method is often misunderstood. While it's true that indiscriminate use of pluck
over select
can lead to performance issues, understanding when to use each can significantly optimize your queries. pluck
shines when you need an array of values from a single column without the overhead of ActiveRecord objects. Conversely, select
is your go-to when those ActiveRecord objects are needed for further manipulation or when chaining more queries.
Pro Tip: Utilize pluck
when working with APIs or background jobs where ActiveRecord object instantiation is unnecessary and could lead to increased memory usage.
2-Using exists?
Instead of any?
or count
When you want to check if any records exist that meet certain criteria, it's more efficient to use exists?
instead of any?
or counting records. exists?
will stop scanning as soon as it finds the first record that matches the condition, which is more efficient than loading multiple records into memory.
Pro tip : Use exists?
for feature toggles or conditional logic in views where you need to check the presence of associated records without loading them
# Less efficient
User.where(active: true).any?
User.where(active: true).count > 0
# More efficient
User.exists?(active: true)
3- Understanding count
vs. size
count
:
The
count
method performs an SQLCOUNT
query against the database.Every time you call
count
, it executes a new SQL query, which can be less efficient, especially on large tables or complex associations.
size
:
The
size
method is more intelligent and adaptive thancount
.If the association has been loaded,
size
will return the result without hitting the database by counting the elements in the loaded array.If the association has not been loaded,
size
will perform an SQLCOUNT
query, similar tocount
.This makes
size
more efficient in scenarios where the records are already loaded into memory because it avoids unnecessary database queries.
Pro tip: Default to size
for a balanced approach to efficiency and accuracy, especially when working with associations that might already be loaded.
Wrapping Up
Optimizing RoR applications involves a blend of understanding ActiveRecord's intricacies and applying best practices judiciously. While syntactic sugar makes RoR delightful, it's the mindful use of its features that ensures both productivity and performance.
Stay tuned for more insights, and remember, the goal is not just to write code but to write code that scales gracefully and efficiently. Happy coding, and let's keep making magic with Ruby 💎!
Subscribe to my newsletter
Read articles from Mohamed Salem directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by