Rails Active Record Best Practices: Query Optimization, Indexing & Batching (2026)
The authoritative guide to Active Record mastery in Ruby on Rails. Partial indexes, subqueries vs joins, batch processing for millions of records, and optimistic vs pessimistic locking.
Writing High-Performance Active Record Queries
Active Record is one of the greatest productivity engines in software development, but it abstracts away the database so effectively that developers frequently forget they are interacting with an ACID relational database. Below are the core principles for writing scalable Active Record code.
1. Memory-Safe Batch Processing for Millions of Records
Calling User.all.each on a table with 500,000 users instantiates half a million Ruby ActiveRecord objects simultaneously, consuming several gigabytes of RAM and triggering massive Garbage Collection stalls:
# ❌ CRASHES PRODUCTION: Loads all rows into RAM at once
Order.where(status: :completed).each { |order| order.send_receipt! }
# ✅ OPTIMIZED: Loads 1,000 records at a time using keyset pagination
Order.where(status: :completed).find_each(batch_size: 1000) do |order|
order.send_receipt!
end
# ✅ FASTEST FOR BULK UPDATES: Updates in SQL without instantiating Ruby models
Order.where(status: :pending).where("created_at < ?", 7.days.ago).in_batches(of: 1000) do |batch|
batch.update_all(status: :expired)
end
2. Selective Column Fetching: pluck and select
If you only need an array of IDs or emails, don't instantiate full ActiveRecord model objects:
# ❌ SLOW: Instantiates 10,000 User objects in Ruby memory
emails = User.where(active: true).map(&:email)
# ✅ FAST: Returns raw Array of strings directly from PostgreSQL driver (25x faster)
emails = User.where(active: true).pluck(:email)
3. Concurrency & Race Condition Prevention: Pessimistic Locking
When handling payment balance debits, inventory deductions, or booking slot reservations, simultaneous HTTP requests can read the same balance and overspend. Use with_lock to issue SELECT ... FOR UPDATE:
class Wallet < ApplicationRecord
def deduct_balance!(amount)
with_lock do # Acquires row-level lock inside database transaction
raise InsufficientFundsError if balance < amount
update!(balance: balance - amount)
end
end
end
Looking for Dedicated Senior Rails Engineers?
Our US-aligned engineering teams write production-grade Rails and PostgreSQL code for venture-backed startups and growing SaaS companies.
Hire Rails Developers in USA →Frequently Asked Questions
What is the difference between find_each and in_batches in Rails?find_each yields individual model records one by one in batches of 1,000. in_batches yields an ActiveRecord relation representing the batch, allowing bulk database operations like update_all or delete_all without instantiating individual Ruby objects.
When should I use counter_cache in Active Record?
Use counter_cache: true whenever you frequently display association counts (e.g. category.products_count) to eliminate costly SELECT COUNT(*) queries in views.
What is the difference between optimistic and pessimistic locking?
Optimistic locking uses a lock_version column and raises an error if two transactions attempt to save changes to the same version. Pessimistic locking locks the database row immediately via FOR UPDATE until the transaction commits.
How do I fix database deadlocks in Active Record?
Deadlocks occur when two transactions acquire locks on multiple records in differing orders. Always sort record IDs before acquiring locks (e.g. [user_a, user_b].sort_by(&:id).each(&:lock!)) so transactions lock resources in identical sequence.