Fix N+1 Queries In Rails: A Developer's Journey To Speed

Learn to detect and fix Rails N+1 queries with real examples. Transform slow apps into fast ones with includes and eager loading.

Jean Emmanuel Cadet
By Jean Emmanuel Cadet Full-Stack Ruby on Rails Developer
Fix N+1 Queries in Rails: A Developer's Journey to Speed

• 4 minutes read

Share with friends
I remember staring at my screen, confused.
The page loaded. No errors flashed. Nothing broke. But something felt wrong.
Then I opened the Rails logs.
A waterfall of SQL queries cascaded down my terminal. One query triggered another. Then another. Then ten more.
My app was drowning in database calls, and I had no idea why.
That was the day I discovered the N+1 query problem. And if you're reading this, you're probably experiencing that same sinking feeling right now.

Don't worry. You're about to learn exactly how to fix it.



What N+1 Queries Really Mean for Your App

Let me show you the code that caused my headache:

@articles = Article.all

Simple, right? Then, in my view:

<% @articles.each do |article| %>
  <%= article.author.name %>
<% end %>

Here's what Rails actually did behind the scenes:

1 query
to fetch all articles
1 query per article to fetch each author

With 10 articles, that's 11 database queries. With 100 articles? 101 queries.
That's the N+1 problem in action. It works perfectly fine in development. But it absolutely destroys performance at scale.


Rails Actually Tells You Everything

Here's the beautiful truth: Rails never hides this from you.
When I finally learned to read my development logs properly, I saw patterns like this:

Admin Load SELECT "admins".* WHERE "admins"."id" = 1
Admin Load SELECT "admins".* WHERE "admins"."id" = 1
Admin Load SELECT "admins".* WHERE "admins"."id" = 1

The same query. Repeated over and over.
That repetition? That's Rails screaming at you.
Once you recognize this pattern, you'll spot N+1 problems instantly. It's like suddenly being able to see the Matrix.


How to Read Logs Like a Pro

Every SQL line in your logs is a clue.
When you see:

Category Load SELECT "categories".*

Rails is telling you: "Somewhere in your view, you called article.categories, but you didn't preload it."
The fix becomes obvious:

includes(:categories)

No guessing. No trial and error. Just reading what Rails is telling you.

If you're still getting comfortable with how Rails connects models, views, and controllers, understanding Rails MVC architecture will make these patterns click even faster.


The ActionText Trap Everyone Falls Into

When you add rich text to your model:

has_rich_text :content

Rails creates a hidden association called rich_text_content.
So when your view renders:

<%= article.content %>

Rails fires off an N+1 query for ActionText::RichText.
I spent hours debugging this before I understood the pattern. The fix:

includes(:rich_text_content)

This catches almost every Rails developer at least once. Now you know the secret.


Active Storage Needs Two Includes, Not One

Here's another gotcha I learned the hard way.
When you attach an image:

has_one_attached :featured_image

Rails uses two separate tables internally:

  • active_storage_attachments
  • active_storage_blobs

If you only preload the attachment, Rails still queries for the blob later.
The correct solution:

includes(featured_image_attachment: :blob)

This applies to profile pictures, avatars, and any file attachment in your app.


Nested Includes: Following the Object Tree

What if your article displays the author's profile picture?

<%= image_tag article.author.profile_picture %>

Rails needs to preload:

  1. The article's author
  2. The author's profile picture attachment
  3. The attachment's blob

Which translates to:

includes(
  author: { profile_picture_attachment: :blob }
)

It looks complex at first glance. But you're just following the chain of associations. Article to author. Author to picture. Picture to blob.
Simple logic, nested syntax.


What a Production-Ready Query Looks Like

After months of refining, here's what my controller query evolved into:

Article.includes(
  :categories,
  :rich_text_content,
  :author,
  author: { profile_picture_attachment: :blob },
  featured_image_attachment: :blob
)

Every single line corresponds to something my view actually uses. Nothing extra. Nothing missing.
This query went from 50+ database calls down to 6.
The page load time dropped from 2 seconds to 200 milliseconds.


The Philosophy Behind the Fix

Rails is built on convention over configuration. Clarity over cleverness.
The same mindset that helps you choose between build and new applies here, too.
Rails wants you to be explicit about your intentions. When you use includes, you're telling Rails: "I know I'll need this data. Please fetch it upfront."
That's not a performance hack. It's just good design.


The Question That Changes Everything

Stop asking: "What should I put inside includes?"
Start asking: "What methods am I calling in my view?"
If your view calls it, Rails must preload it.
That single mental shift will guide you through every N+1 problem you'll ever face.


The Moment You Stop Feeling Like a Beginner

Every Rails developer reaches a turning point.

You stop blindly copying code from Stack Overflow or AI assistants.
You start reading your logs. 
You start understanding what Rails is actually doing beneath the surface.

That's when Rails stops feeling like magic and starts feeling like a tool you truly control.

I remember the first time I fixed an N+1 problem without googling anything. I just read the logs, spotted the pattern, and knew exactly what to add.

That moment felt incredible.

Because suddenly, performance wasn't scary anymore. It was predictable. It made sense.
And that's when I knew I was becoming a real Rails developer.

My advice for you: Keep building. Keep learning. Keep pushing forward.

💌 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:

From My Dev Desk — Code, Curiosity & Coffee

A friendly newsletter where I share: Tips, lessons, and small wins from my dev journey, straight to your inbox.

    No spam. Unsubscribe anytime.