Ruby on Rails Optimization Part 1

Syntactic Optimization

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 SQL COUNT 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 than count.

  • 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 SQL COUNT query, similar to count.

  • 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 💎!