Skip to main content

Rails joins vs includes — The Difference That Kills Performance

joins filters using SQL JOIN. includes prevents N+1 by eager loading. Using the wrong one creates hidden performance problems. Rails 7/8 with SQL output.

A
Raza Hussain
· 3 min read · 14
Rails joins vs includes — The Difference That Kills Performance

joins and includes look similar. They’re not. Picking the wrong one either misses N+1 queries entirely or loads data you don’t need.

What joins Does

joins performs a SQL INNER JOIN for filtering. It does not load the associated data into memory.

Post.joins(:author).where(authors: { verified: true })
# SELECT posts.* FROM posts
# INNER JOIN authors ON authors.id = posts.author_id
# WHERE authors.verified = true

You filtered by the association. But:

posts = Post.joins(:author).where(authors: { verified: true })
posts.each { |post| puts post.author.name }
# N+1 queries — author was NOT loaded

joins tells the database to filter. It does not tell Rails to load the associated record.

What includes Does

includes eager loads the association to prevent N+1:

Post.includes(:author)
# Either:
# SELECT * FROM posts
# SELECT * FROM authors WHERE id IN (1, 2, 3, ...)
# Or a LEFT OUTER JOIN when you reference the association in a where clause
posts = Post.includes(:author)
posts.each { |post| puts post.author.name }
# No N+1 — authors already loaded

The Classic Mistake

# You want verified authors only, no N+1
Post.includes(:author).where(authors: { verified: true })

This works, but Rails switches from two-query loading to a LEFT OUTER JOIN because you referenced the joined table in where. That’s fine. What’s not fine:

Post.joins(:author).each { |post| puts post.author.name }
# N+1 — you joined for filtering but forgot to load

When to Use Which

Use joins when:

  • You only need to filter by an association’s columns
  • You don’t need the associated data in your loop
  • You want an INNER JOIN (records with no association are excluded)

Use includes when:

  • You’ll access the associated data in a loop
  • You want to prevent N+1

Use both when you need both:

Post.joins(:author).includes(:author).where(authors: { verified: true })

Or more cleanly with eager_load (forces a single LEFT OUTER JOIN):

Post.eager_load(:author).where(authors: { verified: true })

eager_load vs preload vs includes

includes picks the strategy automatically:

  • preload always uses two queries (safe for complex joins)
  • eager_load always uses LEFT OUTER JOIN (needed for where on the association)
  • includes picks between them

If you’re filtering on the association column in where, use eager_load explicitly — includes can pick the wrong strategy.

Production Check

Install the Bullet gem. It catches joins used where includes was needed (N+1) and includes used where joins was sufficient (unnecessary eager loading).

gem 'bullet', group: :development

Both waste resources at scale. The wrong one is usually joins followed by association access in a loop.

The Rule

Was this article helpful?

Your feedback helps us improve our content

Be the first to vote!

How We Verify Conversions

Every conversion shown on this site follows a strict verification process to ensure correctness:

  • Compare results on same dataset — We run both SQL and ActiveRecord against identical test data and verify results match
  • Check generated SQL with to_sql — We inspect the actual SQL Rails generates to catch semantic differences (INNER vs LEFT JOIN, WHERE vs ON, etc.)
  • Add regression tests for tricky cases — Edge cases like NOT EXISTS, anti-joins, and predicate placement are tested with multiple scenarios
  • Tested on Rails 8.1.1 — All conversions verified on current Rails version to ensure compatibility

Last updated: October 10, 2026

Try These Queries in Our Converter

See the SQL examples from this article converted to ActiveRecord—and compare the SQL Rails actually generates.

14

Leave a Response

Responses (0)

No responses yet

Be the first to share your thoughts

R

Raza Hussain

Full-stack developer specializing in Ruby on Rails, React, and modern JavaScript. 15+ years upgrading and maintaining production Rails apps. Led Rails 4/5 → 7 upgrades with 40% performance gains, migrated apps from Heroku to Render cutting costs by 35%, and built systems for StatusGator, CryptoZombies, and others. Available for Rails upgrades, performance work, and cloud migrations.

💼 15 years experience 📝 51 posts