Active Record

Purpose

ActiveRecord is the heart of most Rails applications.

Well-designed database access code is fast, maintainable and resilient to growth. Poorly designed access code — N+1 queries, missing indexes, over-fetching — is the most common source of performance problems.

This guide covers patterns for writing efficient, maintainable ActiveRecord code.

Scopes

Scopes encapsulate common queries on a model.

Define Scopes for Common Queries

class User < ApplicationRecord
  scope :active, -> { where(last_active_at: 30.days.ago..) }
  scope :admins, -> { where(role: :admin) }
  scope :by_name, ->(name) { where("name ILIKE ?", "%#{name}%") }
end

Chainable Scopes

Design scopes to be chainable:

User.active.admins.by_name("Alice")

Avoid Default Scopes

Default scopes cause unexpected behaviour when loading and associating records. Prefer explicit scoping at the call site.

Query Optimization

Select Only What You Need

Avoid loading entire tables when you only need a few columns.

# Bad — loads all columns
User.all.each { |u| puts u.email }

# Good — loads only the needed column
User.select(:email).find_each { |u| puts u.email }

Use find_each for Large Datasets

all loads every record into memory at once. Use find_each or find_in_batches for large datasets.

# Bad — loads 100k users into memory
User.all.each { |u| u.send_newsletter }

# Good — batches 1000 at a time
User.find_each(batch_size: 1000) { |u| u.send_newsletter }

Use pluck for Single Columns

When you only need an array of values from one column, use pluck instead of loading records.

# Bad
emails = User.active.map(&:email)

# Good
emails = User.active.pluck(:email)

N+1 Prevention

Identify N+1 Queries

An N+1 query occurs when code loads a collection and then accesses an association on each record individually, generating one query per record.

Use the bullet gem to detect N+1 queries automatically.

Eager Load with includes

Use includes to eager load associations:

# Bad — generates 1 + N queries
Post.all.each { |p| puts p.author.name }

# Good — generates 2 queries
Post.includes(:author).each { |p| puts p.author.name }

Preload vs Eager Load vs Joins

MethodBehaviourUse When
includesSmart loading — uses one query perDefault choice.
association, or JOINs if needed by
conditions.
preloadAlways loads in separate queries.You always want separate
queries (e.g., across shards).
eager_loadUses LEFT OUTER JOIN to load everythingYou need to reference the
in one query.joined table in a WHERE clause.

Indexing Strategy

Index Foreign Keys

Every foreign key used in associations or queries should have an index.

add_index :posts, :author_id
add_index :comments, :post_id

Index Frequently Queried Columns

Columns used in WHERE, ORDER BY, or GROUP BY clauses should be indexed.

Use Composite Indexes for Multi-Column Queries

When querying on multiple columns together, use a composite index:

add_index :users, [:organization_id, :last_active_at]

The order matters: put the most selective column first.

Avoid Redundant Indexes

Indexes have a write cost. Remove indexes that are not used.

Use PgHero or pg_stat_statements to identify unused indexes.

Counter Caches

Use counter caches for frequently accessed counts:

class Post < ApplicationRecord
  belongs_to :author, counter_cache: true
end

class Author < ApplicationRecord
  has_many :posts
end

Add the column via migration:

add_column :authors, :posts_count, :integer, default: 0, null: false

Transactions

Wrap multi-step operations in transactions:

ActiveRecord::Base.transaction do
  user.save!
  account.save!
  log_entry.save!
end

Use save! and create! (with bang) inside transactions so that any failure triggers a rollback.

Related Documents