N+1 Query Intro
Fast recall
Quick revision
Core answer
book.author in a loop can produce eleven queries. Preload the association when the page will use it: preload uses separate association queries, eager_load uses a left outer join, and includes chooses an eager-loading strategy based on the relation. Verify query count; joins alone filters rows but does not guarantee the association is loaded.Whether you know N+1 = 1 query per record for an association, fixed with includes/preload/eager_load, detectable with bullet.
Looping over records and touching an association without includes (1 query per record).
N+1 = 1 query per record for an association (N+1 total). Fix with includes/preload/eager_load. Detect with bullet. Use counter caches for counts.
Interview-ready guide
Interview answer
N+1 is a query-count pattern caused by lazy association loading inside iteration. One query returns N parent records, then touching an unloaded association sends up to one more query per parent. The page works correctly but latency and database load grow with the number of rows displayed.
For example, books = Book.limit(10) issues one query. Calling book.author.name for every book can issue ten more author queries, for eleven total. Book.preload(:author).limit(10) normally uses one query for books and one for all required authors, reducing query count to two.
Rails provides related strategies. preload uses separate queries per named association. eager_load builds a left outer join. includes can use separate loading or a joined shape depending on how the relation references associated tables. The result and conditions decide which is appropriate.
joins(:author) is not itself a preload; it can constrain books by author columns while later book.author still loads lazily. Conversely, preloading a large nested graph that the response never touches wastes memory and data transfer. The fix should match the exact access path.
I identify N+1 through development logs, query instrumentation, or a query-count test, then verify the count stays bounded as N grows. I also inspect memory and returned rows so a query-count win does not hide excessive eager loading. strict_loading can make accidental lazy association access raise during selected flows, turning a hidden performance regression into an explicit failure.
N+1 Query Intro
Direct Answer
An N+1 query occurs when one query loads N parent records and later association access sends another query for each parent. For example, loading ten books and then calling book.author in a loop can produce eleven queries. Preload the association when the page will use it: preload uses separate association queries, eager_load uses a left outer join, and includes chooses an eager-loading strategy based on the relation. Verify query count; joins alone filters rows but does not guarantee the association is loaded.
Deep Dive & Architecture
The parent relation does not know which associations later presentation code will read unless the query declares them. Each model object carries an association cache. The first access can query and cache its own related rows, but ten parent objects still have ten separate caches and therefore ten opportunities to query.
Separate-query preloading first obtains parent keys, then fetches related rows with an IN condition and attaches them to the correct objects. This avoids parent row multiplication. Joined eager loading brings columns together in one result but can repeat parent data for every child and complicate limits or aggregates.
Nested access multiplies the risk. Rendering books, authors, and each author's profile can create another layer of lazy loads. The eager-loading declaration should mirror the associations actually traversed, while serializers and views should avoid accessing surprise relationships far from the query owner.
A bounded query count is the durable assertion. A timing measurement can vary with cache and machine speed, while a test that renders 2 and 20 records and observes the same small query count directly protects the removed growth pattern.
Code Example
# N+1: one books query, then an author query per book.
books = Book.limit(20)
names = books.map { |book| book.author.name }
# Separate-query eager loading: normally two queries total.
books = Book.preload(:author).limit(20)
names = books.map { |book| book.author.name }
# During development, selected relations can reject lazy loading.
books = Book.strict_loading.preload(:author).limit(20)
Common Mistakes & Pitfalls
Looping over records and touching an association without includes (1 query per record).
Check your understanding
Answer these without looking back at the article.
- How do `preload`, `eager_load`, and `includes` differ?
- Why does `joins(:author)` not necessarily prevent lazy author queries?
- How can a query-count test prove the fix still works as N grows?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.