Originally published on Hashnode:
https://railswithyashika.hashnode.dev/rails-performance-n-plus-one-queries
When working with associations in Rails, it's easy to accidentally introduce performance issues. One of the most common problems is the N+1 Query Problem.
In this article, we'll understand what N+1 queries are, how they impact performance, and the differences between includes, preload, and eager_load.
What is an N+1 Query?
Suppose we have the following models:
classUser<ApplicationRecordhas_many:postsendclassPost<ApplicationRecordbelongs_to:userendWe fetch all posts:
@posts=Post.allAnd display the author's name:
<%@posts.eachdo|post|%><%=post.title%><%=post.user.name%><%end%>Queries Generated
Rails first loads all posts:
SELECT"posts".*FROM"posts";Then for each post:
-SELECT"users".*FROM"users"WHERE"users"."id"=1LIMIT1;-SELECT"users".*FROM"users"WHERE"users"."id"=2LIMIT1;-SELECT"users".*FROM"users"WHERE"users"."id"=3LIMIT1;If there are 100 posts, Rails executes:
- 1 query for posts
- 100 queries for users
Total: 101 queries
This is called the N+1 query problem.
Why is it a Problem?
As the amount of data grows:
- Database load increases
- Response times become slower
- More memory and CPU are consumed
- Application scalability decreases A page that works fine with 10 records can become painfully slow with 1,000 records.
Fixing N+1 Queries with includes
The simplest solution is:
@posts=Post.includes(:user)Rails executes:
SELECT"posts".*FROM"posts";Then:
SELECT"users".*FROM"users"WHERE"users"."id"IN(1,2,3,4,5);Only 2 queries are executed regardless of how many posts exist.
Understanding includes
Most Rails developers use includes, but many don't know how it actually works.
Example
Post.includes(:user)Generated Queries
SELECT"posts".*FROM"posts";SELECT"users".*FROM"users"WHERE"users"."id"IN(1,2,3,4,5);Rails loads records using separate queries and associates them in memory.
When Rails Converts includes into JOIN
Consider:
Post.includes(:user).where(users: {active: true})Now Rails generates:
SELECTposts.id,posts.title,users.id,users.nameFROMpostsLEFTOUTERJOINusersONusers.id=posts.user_idWHEREusers.active=true;Because the query references the users table, Rails automatically switches to a JOIN strategy.
Understanding preload
preload always loads associations using separate queries.
Example
Post.preload(:user)Queries Generated
SELECT"posts".*FROM"posts";SELECT"users".*FROM"users"WHERE"users"."id"IN(1,2,3,4,5);Notice that the generated SQL is similar to includes.
Key Difference
Unlike includes, preload never converts into a JOIN.
This will fail:
Post.preload(:user).where(users: {active: true})Error:
missing FROM-clause entry for table "users"
Since no JOIN is generated, Rails cannot reference columns from the users table.
When to Use preload
Use preload when:
- You know separate queries are preferred.
- You only want to avoid N+1 queries.
- You don't need conditions on associated tables.
Understanding eager_load
eager_load always uses a LEFT OUTER JOIN.
Example
Post.eager_load(:user)Query Generated
SELECTposts.id,posts.title,posts.user_id,users.id,users.nameFROMpostsLEFTOUTERJOINusersONusers.id=posts.user_id;Everything is fetched in a single query.
Filtering on Associated Tables
Post.eager_load(:user).where(users: {active: true})Generated SQL:
SELECT"posts".*,"users".*FROM"posts"LEFTOUTERJOIN"users"ON"users"."id"="posts"."user_id"WHERE"users"."active"=TRUE;This works because the users table is already joined.
includes vs preload vs eager_load
| Method | Queries | Uses JOIN | Can Filter Associated Table |
|---|---|---|---|
| includes | Usually 2 | Sometimes | Yes |
| preload | 2 | No | No |
| eager_load | 1 | Always | Yes |
Which One Should You Use?
*Use includes
*Post.includes(:user)
Default choice for most cases.
Use preload
Post.preload(:user)When you explicitly want separate queries and no JOIN.
Use eager_load
Post.eager_load(:user)When filtering, ordering, or searching on associated tables.
Detecting N+1 Queries
A few ways to identify N+1 problems:
Check Development Logs
Look for repeated queries being executed inside loops.
Use Bullet Gem
Add:
gem'bullet'Bullet will notify you whenever an N+1 query is detected.
Use Monitoring Tools
- Scout APM
- New Relic
- Datadog
These tools help identify slow database queries in production.
Conclusion
N+1 queries are one of the most common performance issues in Rails applications.
Understanding the differences between includes, preload, and eager_load can help you write more efficient database queries and build scalable applications.
As a rule of thumb:
- Start with
includes - Use
preloadwhen you want guaranteed separate queries - Use
eager_loadwhen you need JOIN-based filtering
A few minutes spent analyzing your SQL queries can save hours of performance troubleshooting later.


Top comments (0)