Rails Interview #1: The N+1 Query Problem

ruby dev.to

You ship a /posts index page. It renders 50 posts, and for each one it shows the author's name with post.author.name. QA says the page is slow, and the logs are full of repetitive SQL.

  1. What is this problem called, and why is it happening?
  2. How would you detect it, in dev and in a running app?
  3. How do you fix it, and what's the difference between the main fixing strategies?

Answer: N+1 queries

What it is and why it happens

This is the N+1 query problem. One query loads the 50 posts, then Active Record fires one more query per post to load post.author. That's 1 + N queries (1 for posts, 50 for authors) when it could be 2, or even 1. Active Record associations are lazy by default, so the author isn't loaded until you call post.author. Do that inside a loop and you get a round trip per record.

How to detect it

  • Dev logs: you'll see the same SELECT ... FROM authors WHERE id = ? over and over with different IDs. That repetition is the tell.
  • The Bullet gem: built for this. It warns you in dev when you should add eager loading, and also when you're eager-loading something you don't need.
  • An APM in production (Sentry, New Relic, Scout): flags endpoints firing a lot of queries.
  • Tests: assert on query count with something like assert_queries or an RSpec matcher so CI catches a regression before it ships.

How to fix it

Eager-load the association:

# N+1
@posts = Post.all
@posts.each { |p| puts p.author.name }   # 1 + 50 queries

# Fixed
@posts = Post.includes(:author)
@posts.each { |p| puts p.author.name }   # 2 queries
Enter fullscreen mode Exit fullscreen mode

There are four tools, and they don't do the same thing:

Method What it does When to use it
preload Loads the association in a separate query and matches it in memory You just need the data and aren't filtering or ordering by the association
eager_load One LEFT OUTER JOIN that loads everything in a single query You need to filter or order by the association and read its attributes
includes Defaults to preload, but switches to eager_load if you reference the association in a where or order Your usual default. Let Rails decide
joins INNER JOIN for filtering or sorting. Does not load the association into memory You need to filter by the association but don't read its attributes

The trap: joins alone does not preload. If you joins(:author) and then call p.author.name in the loop, you're right back to N+1. Reach for includes or eager_load when you actually read the association's attributes.

Source: dev.to

arrow_back Back to Tutorials