Fix Bullet::Notification::UnoptimizedQueryError: USE eager loading detected Post => [:comments] in Rails
An N+1 query occurs when your code loads a collection and then makes a separate database query for each item's associations. This drastically slows page loads as the number of records grows. Fix it by using includes, preload, or eager_load to batch-load the associated records in a single query.
Reading the Stack Trace
Here's what each line means:
- app/views/posts/index.html.erb:5: The view iterates over posts and accesses post.comments on each iteration, triggering individual queries.
- app/controllers/posts_controller.rb:8:in `index': The controller loads posts without eager loading the comments association.
- activerecord (7.1.3) lib/active_record/relation.rb:860:in `exec_queries': ActiveRecord executes a separate query for each post's comments instead of batching.
Common Causes
1. Missing includes on association
The controller fetches posts but does not eager load comments, causing a query per post when the view accesses comments.
# app/controllers/posts_controller.rb
def index
@posts = Post.all
end
# app/views/posts/index.html.erb
<% @posts.each do |post| %>
<p><%= post.comments.count %> comments</p>
<% end %>
2. Nested associations not preloaded
Eager loading one level but not the nested association still causes N+1 at the deeper level.
@posts = Post.includes(:comments)
# But view also accesses comment.user which is not preloaded
<% post.comments.each do |comment| %>
<p><%= comment.user.name %></p>
<% end %>
3. Counter cache not used
Using association.count instead of a counter cache column forces a COUNT query for each record.
<% @posts.each do |post| %>
<p><%= post.comments.count %></p> <!-- COUNT query per post -->
<% end %>
The Fix
Adding .includes(:comments) tells ActiveRecord to load all comments for all posts in a single query using a LEFT OUTER JOIN or a separate IN query, eliminating the N+1 problem.
def index
@posts = Post.all
end
def index
@posts = Post.includes(:comments).order(created_at: :desc)
end
Testing the Fix
require 'rails_helper'
RSpec.describe PostsController, type: :controller do
describe 'GET #index' do
it 'eager loads comments to avoid N+1' do
posts = create_list(:post, 3)
posts.each { |p| create_list(:comment, 2, post: p) }
expect {
get :index
}.to make_database_queries(count: 2..3)
end
it 'returns all posts' do
create_list(:post, 3)
get :index
expect(assigns(:posts).size).to eq(3)
end
end
end
Run your tests:
bundle exec rspec spec/controllers/posts_controller_spec.rb
Pushing Through CI/CD
git checkout -b fix/rails-n-plus-one,git add app/controllers/posts_controller.rb,git commit -m "fix: add includes(:comments) to eliminate N+1 query",git push origin fix/rails-n-plus-one
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- run: bin/rails db:setup
- run: bundle exec rspec
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
gem install bugstack
Step 2: Initialize
require 'bugstack'
Bugstack.init(api_key: ENV['BUGSTACK_API_KEY'])
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Add includes to the controller query.
- Enable Bullet gem in development and test to detect future N+1 queries.
- Run the test suite and confirm query count is reduced.
- Open a pull request.
- Merge and monitor query performance in production.
Frequently Asked Questions
BugStack runs the fix through your existing test suite, generates additional edge-case tests, and validates that no other components are affected before marking it safe to deploy.
BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.
includes lets Rails decide the strategy. preload always uses separate queries. eager_load always uses LEFT OUTER JOIN. Use eager_load when you need to filter on the association.
Bullet catches most common N+1 patterns but may miss complex cases involving scopes or custom SQL. Pair it with query log analysis for thorough coverage.