The N+1 Query Problem in Active Record
 Imagine you’re building a Rails application that works perfectly in development. You have 10 authors in your database, the app responds quickly, and everything seems fine. But when you deploy to production with 1,000 authors, suddenly pages are taking several seconds to load. What happened?
Chances are, you’ve fallen into one of the most subtle and dangerous traps in Active Record development: the N+1 query problem. This problem is especially treacherous because your Ruby code might look completely correct and idiomatic. There are no errors, no warnings, just… slowness.
The N+1 problem gets its name from the query pattern it generates: first, you run one query to get a set of records (e.g., all authors), and then you run N additional queries, one for each record, to get their related data (each author’s books). The final result is 1+N queries, hence the name.
If you have 10 authors, that’s 11 queries. If you have 100 authors, that’s 101 queries. If you have 10,000 authors… well, you get the picture. Each additional query has a cost: network latency (the round trip to the database), processing time, using a connection from the pool… It all adds up.
In this article, we’re going to analyze four different ways to write the same code, and we’ll see how each one dramatically affects the number of SQL queries that get executed. You might be surprised to find that some solutions that seem correct don’t actually solve the problem at all.
Model Context
For all our examples, we’ll work with a simple but realistic model:
class Author < ApplicationRecord  has_many :booksendThis is a very common relationship: an author can have multiple published books.
Throughout this article, I’ll assume we have 10 authors in our database, and that each author has 3 books. These numbers will make it easy to count the queries, but remember that the impact of the N+1 problem grows linearly with the number of records.
Case 1: The N+1 Problem in its Purest Form
Let’s start with the most natural, intuitive code you’d probably write without thinking too much about performance:
Author.all.each do |author|  author.books.each { |book| puts book.title }endThis code looks perfectly reasonable, right? We’re getting all the authors and then, for each one, iterating over their books and printing the title. It’s clean, readable, idiomatic code… and disastrous for performance.
What’s Happening Under the Hood?
When you run this code, Active Record is going to perform a total of 11 SQL queries. Let’s break them down one by one to understand exactly what’s happening.
The first query happens when you call Author.all. Active Record goes to the database and runs:
SELECT "authors".* FROM "authors"This query returns your 10 authors. So far, so good. We have 10 Author objects in memory, and now we start iterating over them.
This is where things get interesting (or problematic, depending on how you look at it). When your code hits the line author.books.each, Active Record notices that the Book objects associated with this particular author aren’t loaded in memory. So what does it do? It runs a new query against the database:
SELECT "books".* FROM "books" WHERE "books"."author_id" = 1This returns the 3 books for the first author. Perfect. Now you can iterate over them and print each title.
But here’s the problem: this process is repeated for each of the 10 authors. When you get to the second author and access author.books, Active Record again notices those books aren’t loaded, and it runs another query:
SELECT "books".* FROM "books" WHERE "books"."author_id" = 2And then another for author 3:
SELECT "books".* FROM "books" WHERE "books"."author_id" = 3And so on, all the way to author 10. In total: 1 initial query for the authors + 10 queries for the books = 11 SQL queries.
The Real Problem: Scalability
With 10 authors, 11 queries isn’t a huge deal. The database will process them in milliseconds. But the N+1 problem is that it scales linearly with the number of records.
If you have 100 authors, you’re going to run 101 queries. If you have 1,000 authors, it’s 1,001 queries. If you have 10,000 authors (which isn’t an unreasonable number for a publishing platform or digital library), we’re talking about 10,001 queries. Even if each query only takes 5 milliseconds, you’d be looking at over 50 seconds of SQL query time.
But there’s more: each of these queries also uses a connection from your database connection pool. If your pool has 5 connections and you’re running 1,001 queries sequentially, you’re monopolizing one connection for that entire time, which can cause concurrency issues when multiple users are hitting your application at once.
Why Doesn’t Active Record Prevent this Automatically?
You might be wondering, “If this is such a well-known problem, why doesn’t Active Record prevent it automatically?”
When you write Author.all, you’re telling Active Record: “give me all the authors”. You haven’t told it anything about their books. Active Record can’t know that three lines later, you’re going to access those books. It’s waiting for your explicit instructions.
It could try to automatically load all associations for all models “just in case”, but that would be even worse: you’d be loading potentially gigabytes of data you’re never going to use. Active Record is following the principle of “lazy loading”: it only loads data when you actually need it.
The problem is that “when you need it” often means “inside a loop”, and that’s exactly where you don’t want to be making multiple queries.
Case 2: The joins Deception
After learning about the N+1 problem, you might think, “A SQL JOIN will combine the tables and fix everything”, and you’d write something like this:
Author.joins(:books).all.each do |author|  author.books.each { |book| puts book.title }endIt’s a completely logical assumption. After all, it’s the traditional way to combine related tables. Unfortunately, this solution doesn’t work. The N+1 problem persists, exactly the same as in Case 1.
What Does joins Actually Do?
When you run this code, Active Record still makes 11 SQL queries. Let’s see what’s happening.
The first query is different now. Instead of a simple SELECT from the authors table, you get:
SELECT "authors".* FROM "authors"INNER JOIN "books" ON "books"."author_id" = "authors"."id"This is a classic INNER JOIN between the two tables. The query is effectively “joining” the two tables. But there’s something crucial to notice: it’s only selecting columns from the authors table (SELECT "authors".*). No data from the books table is being selected.
This means that even though the two tables are joined in the SQL query, Active Record is only bringing information about the authors into Ruby memory. The book data is used for the JOIN (and could be used for filtering), but it’s not being loaded into Active Record objects.
There’s another interesting side effect: if an author has 3 books, their row will appear 3 times in the JOIN result. But Active Record is smart enough to deduplicate these records and give you just 10 unique Author objects.
The Loop
When your code gets to the loop and accesses author.books, Active Record looks in memory to see if this author’s books are loaded, but they aren’t there.
Remember: the JOIN only loaded author data. Active Record has no way of knowing you’re going to need the book data. So the result is the same as in Case 1: it runs a new query for every single author.
SELECT "books".* FROM "books" WHERE "books"."author_id" = 1SELECT "books".* FROM "books" WHERE "books"."author_id" = 2...And so you end up with 11 queries in total, exactly as before.
So, What Is joins Good For?
This is probably your question right now: “If joins doesn’t prevent N+1, what’s it good for?”
The main purpose of joins is for filtering, not for loading data. That is, it allows you to build queries that depend on conditions in the related tables.
For example, imagine you only want authors who have a book published in 2024:
Author.joins(:books).where(books: { published_year: 2024 })In this case, joins would be ideal. The INNER JOIN lets you put conditions on the books table, and you get back only the authors who meet that criteria.
But if you are then going to access author.books in a loop, you will still need to solve the N+1 problem another way. joins and eager loading are two different tools for two different problems.
A Subtle but Important Difference
There’s one important difference between Case 1 and Case 2 because of the INNER JOIN.
In Case 1, if you have an author who hasn’t published any books, that author will still show up in the results. Author.all gives you all authors, regardless of whether they have books.
In Case 2, with the INNER JOIN, an author without books will not appear in the results. This is because an INNER JOIN only returns rows that have a match in both tables.
So, even though both cases have the same performance problem (11 queries), they might return different authors depending on your data. Keep this difference in mind.
Case 3: includes and Eager Loading
Finally, we arrive at the real solution to the N+1 problem:
Author.includes(:books).all.each do |author|  author.books.each { |book| puts book.title }endThis code runs only 2 SQL queries, no matter how many authors you have. It could be 10 authors, 100, or 10,000… it will always be 2 queries. This is the effect of “eager loading”.
What Does includes Do?
The key is how includes communicates your intentions to Active Record. When you write Author.includes(:books), you’re explicitly telling it: “I’m going to need the books for these authors, so please load them ahead of time”.
Active Record receives this message and adopts a smart, two-query strategy.
The first query is to get all the authors:
SELECT "authors".* FROM "authors"The result is the authors and their IDs, for example: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.
Since Active Record knows you’re going to need the books for these specific authors, before your Ruby code even gets to the loop, it runs a second query:
SELECT "books".* FROM "books"WHERE "books"."author_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)Notice the WHERE ... IN (...). This query gets all the books for all the authors in a single operation. It doesn’t matter if each author has 1 book or 100 books. They all get loaded in this one query.
How Does Active Record Organize the Data?
Active Record loads all these books into memory in an organized way. It sorts them and associates them with their respective authors.
When the second query is complete, Active Record has in memory:
- 10 
Authorobjects - 30 
Bookobjects (3 per author) 
And what’s more, it has already linked them up.
So, when your code gets to the loop and accesses author.books, Active Record looks at the relationship and returns the books from memory, since it already loaded them in the second query.
This process repeats for each of the 10 authors, and at no point is an additional query run. Everything is already loaded in memory.
The Performance Impact
Imagine each SQL query takes 10 milliseconds:
Case 1 (No optimization):
- 10 authors: 11 queries × 10ms = 110ms
 - 100 authors: 101 queries × 10ms = 1,010ms (1 second)
 - 1,000 authors: 1,001 queries × 10ms = 10,010ms (10 seconds)
 
Case 3 (With includes):
- 10 authors: 2 queries × 10ms = 20ms
 - 100 authors: 2 queries × 10ms = 20ms
 - 1,000 authors: 2 queries × 10ms = 20ms
 
The difference is enormous. With 1,000 authors, you’re going from 10 seconds to 20 milliseconds. That’s a 500x improvement.
Including Multiple Associations
Another feature of includes is that you can load multiple associations at the same time:
Author.includes(:books, :articles, :social_profiles)This will prevent N+1 on all three associations simultaneously: if you later access any of them inside the loop, the data will already be in memory.
Case 4: Combining joins and includes
We arrive at an interesting case where we combine both methods:
Author.joins(:books).includes(:books).all.each do |author|  author.books.each { |book| puts book.title }endAt first glance, this might seem redundant. Why would you use joins and includes together on the same association? As we’ve learned, they actually do very different things, and combining them triggers an interesting optimization in Active Record.
What happens with this combination?
This code executes 1 SQL query. When Active Record sees that you’re using both joins and includes on the same association, it’s smart enough to combine them into a single efficient query:
SELECT "authors"."id" AS t0_r0, "authors"."name" AS t0_r1, /* ... */       "books"."id" AS t1_r0, "books"."title" AS t1_r1, /* ... */FROM "authors"INNER JOIN "books" ON "books"."author_id" = "authors"."id"Notice how this query:
- Uses an 
INNER JOIN(so only authors with books are included) - Selects columns from both tables (
t0_r*for authors,t1_r*for books) - Loads everything in a single database round-trip
 
This is more efficient than Case 3, which needed 2 queries. The INNER JOIN both filters and loads the data simultaneously.
When should you use this combination?
The combination of joins and includes is useful in a very specific scenario: when you need to filter based on the association and access that association without N+1.
Imagine this real-world scenario:
# I want authors who have at least one book published in 2024,# and then I need to display ALL their books (including from other years)authors_with_2024_books = Author  .joins(:books)  .where(books: { published_year: 2024 })  .includes(:books)
authors_with_2024_books.each do |author|  puts "#{author.name} has published:"  author.books.each do |book|    puts "  - #{book.title} (#{book.published_year})"  endendIn this example:
joinslets you filter with anINNER JOIN: you only get authors who have at least one book published in 2024.includesprevents N+1: when you accessauthor.booksin the loop, the books are already loaded in memory.
Without the includes, you would have an N+1 problem when accessing author.books inside the loop. Without the joins, you couldn’t filter by conditions on the books table.
The Difference From Case 3
The key functional difference between Case 3 (only includes) and Case 4 (joins + includes) is:
Case 3
- Includes ALL authors, even those without published books
 - Executes 2 queries
 - When you access 
author.booksfor an author with no books, you get an empty array 
Case 4
- Includes ONLY authors who have at least one book, because of the 
INNER JOIN - Executes just 1 query (more efficient!)
 - All data is loaded in a single database round-trip
 
The choice between one or the other depends on your business requirements.
A Note on preload and eager_load
Active Record has two other methods related to eager loading:
preload: Always uses separate queries (like the 2 queries we saw in Case 3). It never uses a JOIN.
Author.preload(:books)  # Guarantees 2 separate querieseager_load: Always uses a LEFT OUTER JOIN. It loads everything in a single SQL query.
Author.eager_load(:books)  # Uses a LEFT OUTER JOINIn most cases, you’ll only need to use includes. Active Record will decide internally which strategy to use based on the query. But if you need fine-grained control over how data is loaded, you can use preload and eager_load manually.
Recap
The following table summarizes the key aspects of each approach:
| Case | Method | Queries | Includes authors without books? | Recommended Use | 
|---|---|---|---|---|
| 1 | all | 1 + N | Yes | Never for accessing associations | 
| 2 | joins | 1 + N | No | Only for filtering, not accessing | 
| 3 | includes | 2 | Yes | For accessing without N+1 | 
| 4 | joins + includes | 1 | No | For filtering and accessing without N+1 | 
The fundamental lesson is that if you’re going to access an association inside a loop, always use includes. The combination of joins and includes gives you additional control over which records are included in your result set (and it’s actually more efficient).
Detecting the N+1 Problem in Your Application
One of the most dangerous aspects of the N+1 problem is that it’s silent. Your application doesn’t throw errors. There are no warnings. Everything works correctly… just very, very slowly at scale. So, how can you detect this problem before it gets to production?
The Bullet Gem
There is a gem called Bullet that detects N+1 problems in Rails applications.
To install and configure it, first add it to your Gemfile, but only for the development group:
# Gemfilegem 'bullet', group: :developmentThen, configure how you want Bullet to notify you when it detects a problem:
config.after_initialize do  Bullet.enable = true  Bullet.alert = true          # Show JavaScript alerts in the browser  Bullet.bullet_logger = true  # Write to log/bullet.log  Bullet.console = true        # Show warnings in the server console  Bullet.rails_logger = true   # Write to the Rails logendOnce configured, Bullet works silently in the background while you develop your application, and it will alert you when it detects an N+1 problem. As you’ve seen, you can choose how you want to receive these alerts: pop-up JavaScript alerts in your browser, messages in the server console, or entries in the log files.
The great thing about Bullet is that it doesn’t just tell you “you have an N+1 problem”, it also suggests exactly how to fix it. It will tell you which association is causing the problem and suggest you use includes or preload.
Recognizing the Pattern in Logs
Even without Bullet, you can spot N+1 problems by looking at your Rails application logs. The Active Record logs show every SQL query that is executed, and the N+1 problem has a very characteristic visual signature.
Look for patterns that look like this in your logs:
Author Load (0.3ms)  SELECT "authors".* FROM "authors"Book Load (0.2ms)    SELECT "books".* WHERE "books"."author_id" = 1Book Load (0.2ms)    SELECT "books".* WHERE "books"."author_id" = 2Book Load (0.2ms)    SELECT "books".* WHERE "books"."author_id" = 3Book Load (0.2ms)    SELECT "books".* WHERE "books"."author_id" = 4Book Load (0.2ms)    SELECT "books".* WHERE "books"."author_id" = 5...See the pattern? One initial query followed by multiple, nearly identical queries that only differ by the ID.
Compare this to what the logs would look like if you used includes:
Author Load (0.3ms)  SELECT "authors".* FROM "authors"Book Load (0.5ms)    SELECT "books".* FROM "books"                     WHERE "books"."author_id" IN (1, 2, 3, 4, 5, ...)Just two queries, no matter how many authors you have.
Counting Queries in Tests
A good practice is to include tests in your test suite that specifically check the number of SQL queries. Rails has built-in tools for this:
assert_queries_count(2) do  get :index  # internally runs Author.includes(:books)endThis test will fail if the number of queries executed is not exactly 2. It’s a proactive way to ensure you don’t accidentally introduce N+1 problems into your code as it evolves.
Conclusion
After this review of the four cases, let’s extract the key lessons.
Use includes to Access Associations
If there’s only one lesson to take away, it’s this: whenever you are going to access an association inside a loop, use includes. Make it your first question when you write code that iterates over models: “Am I going to access any associations here? If so, am I loading them ahead of time?”
Don’t wait for performance problems to appear. Prevention is much easier than correction. Adding includes from the beginning will save you hours of debugging and optimization down the line.
Use joins to Filter
We’ve seen that joins does not prevent the N+1 problem. This is because joins has a different purpose: it’s a filtering tool, not a loading tool.
Think of it this way: joins tells you which records you want (based on conditions in related tables), while includes tells you how much you want to load for each record (including their associations).
When you need both functionalities, combine them:
Author.joins(:books).where(...).includes(:books)Scalability
The 10 authors your application might have at the beginning could be 1,000 next year and 50,000 in five years. And when you get to those numbers, the difference between a constant number of queries (1-2) and 50,001 queries isn’t just a performance difference: it’s the difference between a usable application and one that’s practically unusable.
It’s best to prevent the N+1 problem from the start so your code will scale when the time comes.
Use Tools to Help You Detect the Problem
Don’t just rely on your memory or intuition to catch N+1 problems. Use Bullet in development. Check your logs regularly. Write asserts that count queries.
Remember: the N+1 problem is silent. It doesn’t cause obvious errors. It just manifests as gradual slowness that gets worse over time. Without active detection tools, you could be introducing these problems without even realizing it.
The Path Forward
The N+1 problem is just one of many performance issues that can affect Rails applications. But it’s one of the most common, most costly, and paradoxically, easiest to prevent.
Now that you understand how and why this problem occurs, and how includes, joins, and their combination can solve it, you’re prepared to write Rails applications that scale.
Every time you write a loop over Active Record models, take a mental pause and ask yourself: “Am I accessing associations here?” If the answer is yes, you know what to do.
Test your knowledge
-  
How many SQL queries does this code execute with 100 authors?
Author.all.each { |author| author.books.each { |book| puts book.title } } 
-  
You want to display all authors and their books. Which method prevents the N+1 problem?
 
-  
What’s the main advantage of combining
joinsandincludeson the same association? 
-  
An author without any books is in your database. Which query will include this author in the results?