Performance

Fix Rails N+1 Queries Effectively Using Bullet Gem

BLUF (Bottom Line Up Front): N+1 queries are the primary cause of slow page loads in legacy Rails applications. They occur when the application queries the database once for a collection, and then N additional times for associated records. The solution is detecting them via the Bullet gem and applying includes to enforce eager loading.

Phase 1: The Performance Bottleneck

Glossary entry: Polymorphic N+1 Cascades.

In an Active Record structure without eager loading, iterating over associations triggers a separate SQL query for each record.

Synthetic Engineering Context: The N+1 Crisis

Consider a view rendering a list of 50 Posts, displaying the Author name for each.

<%# The Bad Code %>
<% @posts.each do |post| %>
  <h2><%= post.title %></h2>
  <p>Written by: <%= post.author.name %></p>
<% end %>

If the controller just uses @posts = Post.limit(50), the server log will show a catastrophic query cascade:

# Server Log Output
Post Load (1.2ms)  SELECT "posts".* FROM "posts" LIMIT 50
Author Load (0.4ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT 1 [["id", 1]]
Author Load (0.5ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT 1 [["id", 2]]
# ... 48 more queries ...

Phase 2: Detection and Optimization

The bullet gem automates the detection of these inefficiencies.

Execution: Configuring Bullet

Add Bullet to the development and test groups, and configure it to raise alerts in the logs.

# config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.bullet_logger = true
  Bullet.console = true
end

When you hit the page, Bullet warnings will explicitly state the required fix: USE eager loading: post => [:author] Add to your finder: :includes => [:author]

Execution: The Eager Loading Fix

Modify the controller to preload the associated data in memory. You must choose between includes (which decides between a separate IN query or a LEFT OUTER JOIN) or explicit joins if you need to filter by the association.

# The Optimized Controller
class PostsController < ApplicationController
  def index
    # Eager loading the author association
    @posts = Post.includes(:author).limit(50)
  end
end

The database interaction collapses into exactly two queries:

# Optimized Server Log Output
Post Load (1.2ms)  SELECT "posts".* FROM "posts" LIMIT 50
Author Load (1.5ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (1, 2, 3, 4 ... 50)

Phase 3: Next Steps & Risk Mitigation

Applying includes blindly to massive datasets can cause memory bloat by loading thousands of objects into Ruby’s RAM. In high-volume scenarios, you must combine eager loading with database-level materialization or strict batching (find_each).

Need Help Stabilizing Your Legacy App? If your database CPU is constantly spiking at 100%, you likely have severe N+1 query chains hidden in your views or serializers. Our team at USEO performs deep database audits to resolve these bottlenecks.

Contact us for a Technical Debt Audit