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.
- What is this problem called, and why is it happening?
- How would you detect it, in dev and in a running app?
- 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_queriesor 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 itpreload
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.
답글 남기기