ActiveRecord query optimization and N+1 prevention

Sarah Mitchell Feb 2026
3 tabs
# BAD: N+1 query problem
@users = User.all
@users.each do |user|
  puts user.posts.count  # Fires query for each user!
end

# GOOD: Eager loading with includes
@users = User.includes(:posts).all
@users.each do |user|
  puts user.posts.count  # No additional queries
end

# Multiple associations
@users = User.includes(:posts, :comments, profile: :avatar)

# Conditional eager loading
@users = User.includes(:posts).where(posts: { published: true })

# Using joins for filtering (doesn't load association)
@users = User.joins(:posts).where(posts: { status: 'published' }).distinct

# Preload vs Eager Load
User.preload(:posts)     # Always 2 queries
User.eager_load(:posts)  # Always LEFT OUTER JOIN
User.includes(:posts)    # Smart choice based on conditions

# Select specific columns
User.select(:id, :name, :email)  # Only fetch needed columns

# Pluck for single/multiple columns (returns array, not AR objects)
User.pluck(:email)              # => ["alice@example.com", ...]
User.pluck(:id, :name)          # => [[1, "Alice"], [2, "Bob"]]

# exists? vs present?/any?
User.where(status: 'active').exists?  # Fast COUNT query
User.where(status: 'active').any?     # Loads records into memory
3 files · ruby Explain with highlit

ActiveRecord provides powerful query interface, but naive usage causes N+1 queries. includes eager loads associations in 2-3 queries. joins performs SQL JOINs for filtering. preload always uses separate queries; eager_load forces LEFT OUTER JOIN. I use select to limit columns, reducing memory. find_each and find_in_batches process large datasets efficiently. Scopes chain for composable queries. merge combines scopes. Counter caches avoid COUNT queries. Database indices speed lookups—I add indices on foreign keys and frequently queried columns. explain reveals query plans. Bullet gem detects N+1 in development. Proper query optimization dramatically improves performance, especially at scale.