Eliminate N+1 Queries in Rails with Active Record includes
Learn how Rails Active Record's includes method eliminates N+1 query problems, with a controller example, trade‑offs, and a checklist to verify improvements.
27 Dec 2025, 09:49 UTC

Why N+1 Queries Appear
When you render a list of posts and each post displays its comments, a naive query like Post.all fetches all posts. Then, for every post the view calls post.comments, triggering an extra SELECT to get that post’s comments. With 100 posts you end up with 1 + 100 queries.
How includes Solves the Problem
Calling Post.includes(:comments) tells Active Record to eager‑load the association. Rails issues two queries: one to fetch the posts, another to fetch all comments whose post_id matches the fetched posts. The comments are attached to the in‑memory post objects, so later access to post.comments hits no database.
Worked Example in a Controller
# app/controllers/posts_controller.rb
def index
# Before: causes N+1
# @posts = Post.all
# After: eager loads comments
@posts = Post.includes(:comments).limit(20)
end
In the view you can safely loop:
<% @posts.each do |post| %>
<h3><%= post.title %></h3>
<ul>
<% post.comments.each do |comment| %>
<li><%= comment.body %></li>
<% end %>
</ul>
<% end %>
To verify, start the Rails server (rails s) in a development terminal, watch the log (tail -f log/development.log). With the first version you will see lines like:
Post Load (0.5ms) SELECT posts.* FROM posts LIMIT $1 [['LIMIT', 20]]
Comment Load (0.2ms) SELECT comments.* FROM comments WHERE comments.post_id = $1 [['post_id', 1]]
...
After switching to includes the log shows only two statements:
Post Load (0.4ms) SELECT posts.* FROM posts LIMIT $1 [['LIMIT', 20]]
Comment Load (0.3ms) SELECT comments.* FROM comments WHERE comments.post_id IN (SELECT posts.id FROM posts LIMIT $1) [['LIMIT', 20]]
No per‑post comment queries appear.
Trade‑offs and When to Choose Alternatives
- Eager loading pulls all associated rows into memory. If you only need comments for a small subset of posts, you may waste RAM.
- With polymorphic associations (
belongs_to :commentable, polymorphic: true)includescan generate extra queries because Rails must load each polymorphic type separately. - When you need to filter or order by the association, consider
joinscombined withselector usepreloadwith a scope (Post.preload(:comments -> { where(published: true) }).
Actionable Checklist
- Add the
bulletgem (gem 'bullet') to your Gemfile and runbundle install. It will log N+1 warnings in development. - Run
rack-mini-profileror checklog/development.logwhile exercising the endpoint. - If you see a warning, replace the bare association load with
includes(orpreloadwhen you only need to avoid extra queries). - Deploy to a staging environment, monitor response time with a simple benchmark (
ab -n 100 -c 10 http://staging.example.com/posts) before and after the change. - Keep an eye on memory usage (
ps -o rss -p <pid>) if you notice a spike after eager loading large collections.
By measuring the actual impact in your own logs and adjusting based on real usage patterns, you turn a theoretical performance fix into a measurable improvement.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.