• Home
  • About
  • Portfolio
  • Contact
CodeCurious
  • Home
  • About
  • Portfolio
  • Contact
Go Back

Includes Vs Preload Vs Eager_load In Ruby On Rails

Learn the differences between includes, preload, and eager_load in Ruby on Rails to avoid N+1 queries and optimize performance.

Jean Emmanuel Cadet
By Jean Emmanuel Cadet • Ruby on Rails Developer

Last updated : Jul 10, 2026 • 13 min read

includes vs preload vs eager_load in Ruby on Rails

Last updated : Jul 10, 2026 • 13 min read

Share with friends

If you have spent any time working with Active Record, you have probably run into the N+1 query problem, even if you did not know it had a name at the time. Maybe your app felt fast in development with a handful of records, then slowed to a crawl in production once real data piled up. Nine times out of ten, the culprit is a loop quietly firing off dozens or hundreds of extra database queries.

Rails gives you three tools to fix this: includes, preload, and eager_load. They all solve the same basic problem: loading associated records ahead of time instead of one by one, but they do it in different ways and with different tradeoffs. This is exactly where a lot of Rails developers get confused. The methods look similar, they are often used interchangeably in tutorials, and the differences only really show up once you look at the generated SQL.

In this guide, we will break down what each method actually does under the hood, when Rails picks a JOIN versus separate queries, and how to decide which one fits your use case. By the end, you will be able to look at a slow Rails view, diagnose the N+1 problem, and fix it with confidence instead of guessing.


Understanding the N+1 Query Problem

What It Is

The N+1 query problem happens when your code runs one query to fetch a list of records, then runs an additional query for each record to fetch a related association. If you fetch 50 articles and then loop through them to print each author's name, and that author lookup was not eager-loaded, Rails will run one query for the articles and then 50 more queries, one per article, to fetch each author. That is 51 queries total instead of 2.

Why It Hurts Performance

Every query has overhead: opening a connection to the database, parsing SQL, executing it, and returning results back to your Rails process. When that overhead is multiplied by hundreds of records, page load times balloon. In production, this often does not show up until your dataset grows past what you tested locally, which is why N+1 issues are notorious for slipping through code review.

Real-World Example

Imagine a blog with an Article model that belongs to an Author.

class Article < ApplicationRecord
belongs_to :author
end

class Author < ApplicationRecord
has_many :articles
end

Now consider this view code:

<% Article.all.each do |article| %>
<p><%= article.title %> by <%= article.author.name %></p>
<% end %>

SQL Before Optimization

Running this code produces the following queries:

SELECT * FROM articles;

SELECT * FROM authors WHERE id = 1;
SELECT * FROM authors WHERE id = 2;
SELECT * FROM authors WHERE id = 3;
-- ...one query per article

If you have 100 articles, that is 101 total queries just to render a simple list. This is the exact scenario includes, preload, and eager_load are designed to prevent.


What Does includes Do?

includes is the most commonly used eager loading method in Rails, and it is intentionally a bit smart. It decides at runtime whether to load associations using two separate queries or a single query with a JOIN, depending on how you use the association afterward.

How It Works

articles = Article.includes(:author)
articles.each do |article|
puts article.author.name
end

By default, includes runs two separate queries:

SELECT * FROM articles;
SELECT * FROM authors WHERE id IN (1, 2, 3, ...);

Rails fetches all the articles, collects the author IDs, and then runs a single query to grab all the matching authors. Active Record then stitches everything together in memory so calling article.author does not trigger a new query.

When Rails Chooses Separate Queries

If you are only using the association for reading data, without filtering or ordering on its columns, includes stick with the two-query approach shown above. This is efficient and keeps the queries simple.

When Rails Automatically Switches to a JOIN

If you reference the associated table in a where clause or an order, Rails switches strategy and generates a LEFT OUTER JOIN instead.

Article.includes(:author).where(authors: { active: true })

This produces:

SELECT articles.*, authors.* 
FROM articles
LEFT OUTER JOIN authors ON authors.id = articles.author_id
WHERE authors.active = true;

This automatic switching is exactly why includes confuses so many developers. The same method call can behave completely differently depending on the rest of your query chain.

Practical Example

# Two queries, no JOIN
Article.includes(:author).to_a

# Single query with JOIN, because of the where clause
Article.includes(:author).where(authors: { active: true }).to_a

What Does preload Do?

preload is the more predictable, no-surprises sibling of includes. It always uses separate queries and never generates a JOIN, no matter what you do afterward.

How It Works Internally

articles = Article.preload(:author)

This always produces:

SELECT * FROM articles;
SELECT * FROM authors WHERE id IN (1, 2, 3, ...);

Separate Queries Only

Even if you try to filter on the associated table, preload will not switch to a JOIN. In fact, doing so will raise an error because the associated table is not part of the query yet.

# This will raise an ActiveRecord::StatementInvalid error
Article.preload(:author).where(authors: { active: true })

Advantages

  • Predictable behavior, always two queries, never a surprise JOIN.
  • Each query stays simple, which can be easier to read in logs.
  • Works well when you only need the association for display purposes, not for filtering.

Limitations

  • Cannot filter or order by columns on the preloaded association.
  • Not ideal when you need to combine loading and filtering in a single step.

Code Example

# Good use case: displaying author names in a list, no filtering needed
@articles = Article.preload(:author)

What Does eager_load Do?

eager_load is the opposite of preload. It always forces a single query using a LEFT OUTER JOIN, regardless of whether you filter on the association or not.

LEFT OUTER JOIN Behavior

articles = Article.eager_load(:author)

Generated SQL

SELECT articles.*, authors.*
FROM articles
LEFT OUTER JOIN authors ON authors.id = articles.author_id;

Because it is a LEFT OUTER JOIN, articles without an author are still included in the results, with NULL values for the author columns.

Advantages

  • Guarantees a single query, which can be useful when you know you need to filter or sort by the association.
  • Works well when you need to combine the main model and its association in one dataset for complex where conditions.

Limitations

  • A single large JOIN query can return a lot of duplicated data, since every article row now repeats the author columns.
  • For associations with many records, this can be less efficient than two smaller, separate queries.
  • Overusing eager_load when you do not need JOIN, the behavior wastes database resources.

Code Example

# Good use case: filtering articles by an author attribute
Article.eager_load(:author).where(authors: { active: true })

includes vs preload vs eager_load

Here is how the three methods stack up against each other once you look past the surface similarities.

Feature

includes

preload

eager_load

Query strategy

Two queries, or JOIN if needed

Always two separate queries

Always a single LEFT OUTER JOIN

Can filter by association columns

Yes, triggers a JOIN

No, raises an error

Yes, built for this

Predictability

Conditional, depends on query chain

Always predictable

Always predictable

Risk of duplicate data

Low, unless it switches to JOIN

None

Higher, since JOIN can duplicate rows

Best for

General-purpose eager loading

Simple display-only associations

Filtering or ordering by association data

Memory usage

Moderate

Lower, smaller separate result sets

Higher, wider result set from the JOIN

Query Count

preload and the default includes behavior both keep query count low and predictable, at two queries per association. eager_load and JOIN-mode includes collapse everything into one query, which sounds efficient but can actually return more data than needed because of duplicated columns.

Memory Usage

JOIN-based approaches can use more memory because Active Record has to hydrate a wider result set, with the associated table's columns repeated across every row of the parent table. Separate queries avoid this repetition.

Performance Trade-offs

There is no universal winner here. Two smaller queries are sometimes faster than one large JOIN, especially when the associated table is large or when you are joining multiple associations at once. The right choice depends on your access pattern, not on which method sounds more advanced.


When Should You Use Each One?

A simple decision guide:

  • Use includes as your default. It is smart enough to switch to a JOIN automatically when you filter by the association, and stays with separate queries otherwise.
  • Use preload when you know you only need the association for display, and you want to guarantee simple, separate queries with zero surprises.
  • Use eager_load when you specifically need to filter, sort, or query against columns on the associated table, and you want that guaranteed in a single query.

Common Scenarios

  • Rendering a list of blog posts with author names: preload or includes without filtering.
  • Filtering articles where the author is active: eager_load, or includes combined with a where clause on the association.
  • API endpoint returning nested JSON with several associations: includes with multiple associations, letting Rails decide the strategy per association.

Real Application Example

class ArticlesController < ApplicationController
def index
# Straightforward listing, no filtering on author
@articles = Article.preload(:author, :category)
end

def active_authors
# Filtering by author attribute, needs a JOIN
@articles = Article.eager_load(:author).where(authors: { active: true })
end
end

Common Mistakes

Assuming includes Always Performs a JOIN

Many developers assume includes always generates a single query with a JOIN. In reality, it only does this when your query chain references the associated table's columns. Otherwise, it quietly runs two separate queries.

Loading Unnecessary Associations

# Wasteful if comments are never used in the view
Article.includes(:author, :category, :comments)

Only eager load what you actually use. Loading associations you never touch adds unnecessary query time and memory usage.

Using eager_load Unnecessarily

# Unnecessary JOIN if you are not filtering by author
Article.eager_load(:author)

If you do not need to filter or sort by the association, eager_load just adds overhead compared to preload or plain includes.

Ignoring Bullet Gem Warnings

The Bullet gem detects N+1 queries in development and logs warnings, but many developers get used to seeing them and stop paying attention. Treat every Bullet warning as something to investigate before deploying.

Over-Eager Loading

Eager loading every association on every controller action "just in case" adds unnecessary database load. Eager load based on what a specific action actually renders, not based on what the model could theoretically need.


Performance Tips

Selecting Only Needed Columns

Article.select(:id, :title, :author_id).includes(:author)

Pulling only the columns you need reduces the amount of data Active Record has to hydrate into objects.

Combining Eager Loading with Scopes

class Article < ApplicationRecord
scope :with_author_and_category, -> { includes(:author, :category) }
end

Article.published.with_author_and_category

Wrapping eager loading in scopes keeps controllers clean and makes the intent explicit.

Using Bullet

Add the Bullet gem to your development and test environments to get automatic alerts whenever an N+1 query slips into your code.

# Gemfile
group :development do
gem "bullet"
end

Monitoring SQL Logs

Keep an eye on your Rails server logs during development. Every query Active Record generates is printed there, which makes it easy to spot repeated queries hitting the same table.

Benchmarking Queries

Use ActiveSupport::Notifications or simple Benchmark.measure blocks around suspect code paths to confirm that your optimization actually reduced query time, rather than assuming it did.

Benchmark.measure do
Article.includes(:author).to_a
end

Real-World Example

Let's build out a small blog example with Article, Author, Category, and Comment models.

class Article < ApplicationRecord
belongs_to :author
belongs_to :category
has_many :comments
end

class Author < ApplicationRecord
has_many :articles
end

class Category < ApplicationRecord
has_many :articles
end

class Comment < ApplicationRecord
belongs_to :article
end

Original Inefficient Implementation

<% Article.all.each do |article| %>
<h2><%= article.title %></h2>
<p>By <%= article.author.name %> in <%= article.category.name %></p>
<p><%= article.comments.count %> comments</p>
<% end %>

For 50 articles, this generates roughly 1 plus 50 for authors, plus 50 for categories, plus 50 for comment counts, which is around 151 queries.

Optimized With includes

@articles = Article.includes(:author, :category, :comments)
SELECT * FROM articles;
SELECT * FROM authors WHERE id IN (...);
SELECT * FROM categories WHERE id IN (...);
SELECT * FROM comments WHERE article_id IN (...);

This drops the query count from around 151 down to 4, regardless of how many articles you have.

Optimized With preload

@articles = Article.preload(:author, :category, :comments)

Since we are not filtering by any of these associations, preload produces the exact same four queries as includes in this case, with the guarantee that it will never switch to a JOIN.

Optimized With eager_load

@articles = Article.eager_load(:author, :category, :comments)
SELECT articles.*, authors.*, categories.*, comments.*
FROM articles
LEFT OUTER JOIN authors ON authors.id = articles.author_id
LEFT OUTER JOIN categories ON categories.id = articles.category_id
LEFT OUTER JOIN comments ON comments.article_id = articles.id;

This works, but because comments is a has_many association, this JOIN multiplies the article, author, and category columns across every comment row, which can quickly become wasteful for articles with a lot of comments.

Which Solution Is Best and Why

For this scenario, preload or plain includes is the better choice. Both keep the query count low without duplicating data across multiple comment rows. eager_load would only make sense here if you needed to filter articles by something like a comment's content or an author's active status in the same query.


Frequently Asked Questions

When should I use includes? Use includes as your default eager loading method for most situations. It handles simple display cases with two queries and automatically switches to a JOIN if you filter by the association, which removes the need to think about it in many everyday cases.

Does preload always execute two queries? Yes, for each association you preload, Rails runs a separate query. preload never merges these into a JOIN, which is exactly why you cannot filter on the association's columns when using it.

Why does eager_load use LEFT OUTER JOIN? eager_load uses a LEFT OUTER JOIN so that records without an associated row are still included in the result set, with null values in place of the missing association's columns. This makes it possible to filter or sort using the association's data within a single query.

Is includes slower than preload? Not inherently. When includes does not need to switch to a JOIN, it runs the exact same separate queries that preload does. The performance difference only shows up if your query chain forces includes into JOIN mode when you did not intend it to.

Can includes eliminate every N+1 query? It eliminates N+1 queries for the associations you explicitly include. It will not help with associations you forget to eager load, or with nested associations accessed several levels deep, unless you eager load those levels too.

Which method should I use in Rails 8? The core behavior of includes, preload, and eager_load has not fundamentally changed in Rails 8. The same guidance applies: default to includes, reach for preload when you want guaranteed separate queries, and use eager_load when you need guaranteed JOIN behavior for filtering or sorting.


Conclusion

includes, preload, and eager_load all exist to solve the same core problem, avoiding N+1 queries by loading associations ahead of time instead of one record at a time. The difference comes down to how each one gets there. includes adapts based on your query chain, preload always runs separate queries, and eager_load always forces a single JOIN.

There is no single correct answer for every situation. The best approach is to understand what each method actually does under the hood, look at the SQL your code generates, and choose based on whether you need to filter by an association or simply display it. When in doubt, turn on your Rails query logs, install the Bullet gem, and benchmark before and after. That habit alone will save you from most Rails performance headaches, long before they show up in production.

💌 Don’t miss out! Join my newsletter for web development tips, tutorials, and insights delivered straight to your inbox.

Thanks for reading & Happy coding! 🚀

Follow me on:

Code. Learn. Grow.

A friendly newsletter sharing dev tips, lessons, and wins from my journey.

    Enter valid email address

    Services Tailored to Your Needs


    coding

    Web & Mobile Development

    Custom websites and mobile apps built to be fast, modern, and user-friendly. From sleek landing pages to full-scale applications, I deliver solutions that engage your audience and grow your business.

    API development

    Seamlessly connect your systems with secure, scalable APIs. I design and integrate APIs that improve efficiency, reliability, and flexibility for your business processes.

    Database design and management

    Reliable database solutions tailored to your needs. I design, optimize, and maintain databases that ensure performance, security, and scalability for your applications.

    You might also like…

    How Fragment Caching Works in Ruby on Rails
    Rails Tutorials

    How Fragment Caching Works In Ruby On Rails

    By Jean Emmanuel Cadet
    Published on: Jul 08, 2026
    Is Ruby on Rails Still Worth Learning in 2026?
    Learning Concepts

    Is Ruby On Rails Still Worth Learning In 2026?

    By Jean Emmanuel Cadet
    Published on: Jul 06, 2026
    Fetch API vs Axios: Which Should You Use in 2026?
    Web Development

    Fetch API Vs Axios: Which Should You Use In 2026?

    By Jean Emmanuel Cadet
    Published on: Jun 27, 2026
    CodeCurious

    Designed for those who view software as architecture and code as literature.

    Legal

    Terms & Conditions Privacy Policy Disclaimer

    CodeCurious © 2025 - 2026. All rights reserved. | Made with ♥ by @jecode93