- Home
- Blog
- Joins & Associations
- Rails joins vs includes — The Difference That Kills Performance
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.
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
- Filtering by association: joins
- Accessing association data: includes
- Both: eager_load or joins + includes For more on orm, read 7 Production-Safe Ways to Do a SQL
CROSS JOINin Rails (and When You Actually Should).
Was this article helpful?
Your feedback helps us improve our content
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.
Deep Dive into ActiveRecord
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.
More on Joins & Associations
ActiveRecord where vs find_by — Which to Use When
where returns a relation, find_by returns one record or nil. Here's when each is correct, what breaks if you pick wrong, and Rails 7/8 production patterns.
5-Table ActiveRecord Join: 8s → 200ms With Subquery Rewrite
A 5-table ActiveRecord join took 8 seconds. Rewriting it as a subquery took it to 200ms. Here's what changed and why the query planner behaves differently.
Rails Counter Cache—When Posts.count Brought Down Production
posts.count hit the database on every request and brought down production. Here's how counter cache fixes it, what to watch for, and how to backfill safely.
Leave a Response
Responses (0)
No responses yet
Be the first to share your thoughts