Jonathan Bennett

N+1 Queries Aren’t Evil—Here’s How to Use Them Right

The general recommendation is to avoid “n+1” database queries. What does that look like?

Typically, you make one database request, and then an additional request for each result. For example, fetching Post records and their Comments:

class Post < ApplicationRecord
  has_many :comments
end

class Comment < ApplicationRecord
  belongs_to :post
end


<% Post.all.each do |post| %>
  <div>
    <%= post.body %>
    <%= render post.comments %>
  </div>
<% end %>

This approach can cause serious performance issues as the number of posts grows or as each post becomes more complex.

Step 1: Eager Loading

The simplest fix is to eager load the associations using includes:

Post.includes(:comments).each

This reduces the queries to just two:

  1. One query to fetch the posts.
  2. One query to fetch all comments for those posts.

However, while this improves database performance, it can significantly increase memory usage.

Step 2: Leverage Russian Doll Caching

Instead of just avoiding “n+1” queries, you can improve both database and render performance by caching individual components. Here’s how:

Break Components into Partials:

<%# posts/index.html.erb %>
<%= render partial: "posts/post", collection: Post.all, cached: true %>

<%# posts/_post.html.erb %>
<div>
  <%= post.body %>
  <%= render partial: "comments/comment", collection: post.comments, cached: true %>
</div>

With Russian Doll Caching, Rails:

  1. Renders each post and its comments once.
  2. Saves the comments using the comment’s id and updated_at as the cache key.
  3. Saves the post to the cache using the post’s id and updated_at as the cache key.
  4. Serves the cached version on subsequent requests without querying the database.

Step 3: Keep Caches Fresh

If a post is updated, Rails generates a new cache because the updated_at timestamp changes.

To handle changes to comments, ensure the post’s updated_at updates when a comment changes:

class Comment < ApplicationRecord
  belongs_to :post, touch: true
end

This ensures that adding or updating a comment invalidates the post’s cache, keeping everything fresh.

By using Russian Doll Caching, you can avoid most “n+1” queries, reduce memory usage, and keep your app performant as it scales.