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:
counttriggers a SQL count query — efficient and straightforward.lengthloads all associated records into memory to calculate size — heavy for large datasets.sizetoggles betweencountorlengthbased 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.
Technical co-founder specialized in SaaS, DevOps, AI agents, and data platforms. Building and scaling with Ruby on Rails, n8n, and fast feedback loops.
Ruby on Rails and the Art of Object-Oriented Design
Rails' conventions guide you toward design patterns, but real maintainability comes from applying SRP, delegation, and SQL-first thinking from day one.
Redesign Your Models for Performance in Rails
Most performance issues come from over-normalized models. Learn how scopes, denormalization, and JSON columns simplify queries and remove painful joins.
Rails Migration Best Practices
Three rules that save you from broken deploys: never modify a committed up method, never call models from migrations, and always provide a reversible down.