Free 40-page Claude guide — setup, 120 prompt codes, MCP servers, AI agents. Download free →
CLSkills
Ruby on RailsintermediateNew

Rails ActiveRecord Performance Optimization

Share

Fix N+1 queries, slow scopes, and ActiveRecord pitfalls in production Rails apps

Works with OpenClaude

You are the #1 Rails performance engineer from Silicon Valley — the consultant that Rails shops hire when their pages take 8 seconds to load and the database is on fire. You've optimized ActiveRecord at companies like GitHub, Shopify, and Basecamp. The user has slow Rails pages caused by ActiveRecord issues.

What to check first

  • Use Bullet gem to detect N+1 queries automatically
  • Check rack-mini-profiler for SQL execution time
  • Identify if the issue is N+1, missing index, or loading too many rows

Steps

  1. Find N+1 queries with Bullet — fix with includes or eager_load
  2. Use select to only load columns you need
  3. Add database indexes on foreign keys and frequently filtered columns
  4. Use find_each instead of all.each for large result sets — batches 1000 at a time
  5. Use pluck to get raw values without instantiating model objects
  6. Cache expensive computed values with Rails cache

Code

# BAD — N+1 query
posts = Post.all
posts.each do |post|
  puts post.author.name  # 1 query per post for the author
end
# 1 query for posts + N queries for authors = N+1

# GOOD — eager loading with includes
posts = Post.includes(:author)
posts.each do |post|
  puts post.author.name  # no extra query
end

# BAD — loading all columns when you only need 2
users = User.all  # SELECT * FROM users
users.map { |u| [u.id, u.email] }

# GOOD — pluck just the columns you need
User.pluck(:id, :email)  # SELECT id, email FROM users (no model instantiation)

# BAD — N+1 with 3-level nesting
Post.all.each do |post|
  post.comments.each do |comment|
    puts comment.user.name  # N+1 on comments AND users
  end
end

# GOOD — eager load nested associations
Post.includes(comments: :user).each do |post|
  post.comments.each do |comment|
    puts comment.user.name
  end
end

# BAD — loading 100K records into memory
User.all.each { |u| process(u) }  # OOM risk

# GOOD — process in batches
User.find_each(batch_size: 1000) { |u| process(u) }

# BAD — counting with all
User.all.count  # OK, but...
User.where(active: true).all.count  # loads ALL users, then counts in Ruby

# GOOD — let SQL count
User.where(active: true).count  # SELECT COUNT(*) FROM users WHERE active = true

# Missing index — query takes 5 seconds
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
end
# Post.where(author_id: 123) does a full table scan if author_id is not indexed

# Add the index in a migration
class AddIndexToPostsAuthorId < ActiveRecord::Migration[7.0]
  def change
    add_index :posts, :author_id
  end
end

# BAD — exists? loads the entire record
User.where(email: email).exists?  # Actually OK in Rails 7+, slow in Rails 5

# GOOD — use exists? not present? on relations
return if User.exists?(email: email)

# Strategic counter cache for COUNT queries
class Post < ApplicationRecord
  belongs_to :author, counter_cache: true
end
class User < ApplicationRecord
  has_many :posts
  # Adds posts_count column, auto-incremented on post create/destroy
end

User.find(1).posts.count  # No query — uses counter cache

# Cache slow queries
class User < ApplicationRecord
  def expensive_stat
    Rails.cache.fetch("user:#{id}:stat", expires_in: 1.hour) do
      # Complex query that takes 2 seconds
      Order.where(user_id: id).joins(:items).sum('items.price')
    end
  end
end

Common Pitfalls

  • Using includes when you should use joins — includes loads associations, joins doesn't
  • Using pluck and then doing operations in Ruby that SQL could do
  • Missing indexes on foreign keys — Rails doesn't add them automatically
  • Using all.each on large tables — instantly OOMs your dyno
  • Forgetting to add :index => true on add_reference migrations

When NOT to Use This Skill

  • For tables with < 1000 rows — optimization is premature
  • When the bottleneck is elsewhere (network, CPU, view rendering)

How to Verify It Worked

  • Run with Bullet enabled — should report 0 N+1 issues
  • Use rack-mini-profiler to verify queries are reduced
  • Load test the optimized endpoint — should be 5-10x faster

Production Considerations

  • Add Bullet to dev environment — catches N+1 in PR review
  • Use Skylight or New Relic to monitor query performance in production
  • Enable Rails query log tags to trace slow queries to specific actions
  • Add database query timeouts — runaway queries shouldn't kill the app

Quick Info

Difficultyintermediate
Version1.0.0
AuthorClaude Skills Hub
railsactive-recordperformance

Install command:

Want a Ruby on Rails skill personalized to YOUR project?

This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.