RL ROLAND LOPEZ
// 2 min read

Rails ORM Performance Best Practices

Rails ORM Performance Best Practices — ActiveRecord makes it easy to ship slow code. Leverage SQL for sorting, filtering, and aggregation - and fix N+1 queries with includes - to keep Rails apps fast.

Not leveraging SQL effectively

ActiveRecord simplifies database interactions, but misusing its API can lead to severe performance bottlenecks.

Each of count, length, and size serves a different purpose:

  • count triggers a SQL count query — efficient and straightforward.
  • length loads all associated records into memory to calculate size — heavy for large datasets.
  • size toggles between count or length based on data loading. Use with caution.

Directly using SQL functions through ActiveRecord can often bypass performance issues found in Ruby-level list manipulations.

Case study: SQL for sorting and filtering

Suppose you need the first five users, alphabetically by name, from an account:

Inefficient Ruby approach:

@users = @account.users.sort_by { |user| user.name.downcase }.first(5)

While it is intuitive for Rubyists, this approach consumes more memory by loading all users into server memory.

Efficient SQL approach:

@users = @account.users.order('LOWER(name)').limit(5)

Using order and limit directly in SQL is far more efficient, avoiding unnecessary Ruby loops and object loads.

Anti-pattern: calculating collaborators in Ruby

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :comments
  has_many :articles, through: :comments

  def collaborators
    articles.collect { |a| a.users }.flatten.uniq.reject { |u| u == self }
  end
end

Loading and processing large datasets in Ruby is less efficient due to object instantiation and array operations.

Optimal solution: use SQL

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :comments
  has_many :articles, through: :comments

  def collaborators
    User.joins(comments: { article: :comments })
      .where('articles.id IN (?) AND users.id != ?', article_ids, id)
      .distinct
  end
end

This produces:

SELECT DISTINCT users.* FROM users
  INNER JOIN comments ON comments.user_id = users.id
  INNER JOIN users users_comments ON users_comments.id = comments.user_id
  INNER JOIN articles ON articles.id = comments.article_id
  INNER JOIN comments comments_articles ON comments_articles.article_id = articles.id
  WHERE (articles.id in (1) AND users.id != 1)

By using joins and where, complex queries run in the database — significantly improving performance.

N+1 queries

Consider the following set-up:

# app/models/post.rb
class Post < ApplicationRecord
  has_many :comments
end

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end
# app/views/index.html.erb
<%= render @posts %>

# app/views/posts/_post.html.erb
<%= render @posts.title %>
<%= render @posts.comments %>

# app/views/comments/_comment.html.erb
<%= comment.content %>

When rendering the views, <%= render @posts.comments %> iterates through all posts and creates a query for each <%= comment.content %> partial.

The solution: use Post.includes(:comments).

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    # Perf gains
    @posts = Post.includes(:comments)
  end
end

This creates a JOIN in SQL with the comments table and loads everything in one DB request instead of N+1.

Conclusion

Understanding and using SQL’s full capabilities inside Rails applications is crucial for performance.

Always consider SQL as your first solution for data manipulation tasks in Rails, reserving Ruby for application logic where SQL is not sufficient.

Efficient code leverages the strengths of both Ruby and SQL appropriately.

Roland Lopez
Written by
Roland Lopez

Technical co-founder specialized in SaaS, DevOps, AI agents, and data platforms. Building and scaling with Ruby on Rails, n8n, and fast feedback loops.

Built by Agent Skynet See the agency