/ Tags: RAILS-CODE / Categories: SOLUTIONS

Eager Load A Rails Association Only When It Exists To Avoid N1

includes(:profile) on a has_one loads a row per record even when most records have no profile. Filtering to the records that actually have the association, or loading it conditionally, keeps the query cheap without reintroducing N+1.

Description

Use where.associated(:profile) (Rails 7+) for “only records that have one” — it builds an INNER JOIN. where.missing(:profile) gives you the opposite via a LEFT JOIN with a NULL check, which is the clean replacement for a hand-written LEFT OUTER JOIN ... IS NULL. When you need all records but only some have the association, preload is the right tool: it issues one extra query for the associations that exist, and leaves the rest nil. It never joins, so it can’t drop rows. Avoid conditional eager loading based on a Ruby-side check — that’s where N+1 comes back.

Sample input:

  User.where.associated(:profile)
  User.where.missing(:profile)


Sample Output:

  SELECT users.* FROM users INNER JOIN profiles ON profiles.user_id = users.id
  SELECT users.* FROM users LEFT OUTER JOIN profiles ON profiles.user_id = users.id
    WHERE profiles.id IS NULL

Answer

  # Only records that HAVE the association (Rails 7+)
  User.where.associated(:profile)

  # Only records that DON'T
  User.where.missing(:profile)

  # All records, association preloaded where present — two queries, no joins
  users = User.preload(:profile)
  users.each { |u| u.profile&.city }   # no extra queries

  # Conditional preload based on what the page needs
  scope = User.all
  scope = scope.preload(:profile) if params[:include_profile]

  # Preload a filtered association without breaking the parent query
  class User < ApplicationRecord
    has_many :orders
    has_many :recent_orders,
             -> { where(created_at: 30.days.ago..) },
             class_name: "Order"
  end

  User.preload(:recent_orders)

  # Filter on the association AND load it — needs eager_load, not preload
  User.eager_load(:profile).where(profiles: { country: "NP" })

Learn More

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More