The N+1 query problem is a common performance anti-pattern in database-backed applications where code executes one query to fetch a list of records and then fires an additional query for each record to retrieve related data, resulting in N+1 total database round-trips instead of one efficient query.
The N+1 problem occurs when an application fetches a collection of N parent records with a single query, then issues one separate query per record to load associated data — totalling N+1 queries. For example, loading 100 blog posts and then querying the author for each post individually produces 101 database calls. This pattern often emerges silently in ORMs that lazy-load relationships by default.
Each database query carries network latency, connection overhead, and processing cost, so N+1 queries can degrade response times dramatically as data grows. A page that loads fine with 10 records may become unacceptably slow with 1,000. Because the extra queries are generated automatically by framework code, the problem is easy to miss during development but expensive in production.
ORMs like ActiveRecord, Hibernate, SQLAlchemy, and Prisma lazy-load associations by default — meaning related records are only fetched when the property is accessed in code. Iterating over a collection and touching a relationship attribute inside the loop triggers one SQL SELECT per iteration. The code looks clean and simple, which is exactly why the problem hides so well.
The standard solution is eager loading, where you instruct the ORM to JOIN or batch-fetch related data in the original query. In Rails this is `includes`, in Django it is `select_related` or `prefetch_related`, and in Hibernate it is `JOIN FETCH`. This collapses N+1 queries into one or two efficient queries regardless of how many records are in the collection.
Query logging tools and APM solutions can reveal N+1 patterns by showing repeated identical SQL statements differing only in an ID parameter. Libraries like Bullet (Rails), Hibernate Statistics, or Django Debug Toolbar flag eager-loading opportunities automatically. Adding query-count assertions to integration tests is a proactive way to prevent regressions from being merged.
The fix for N+1 queries — eager loading everything — can itself become a problem if you load large associations that are not always needed, wasting memory and bandwidth. Load only the relationships required for a specific use case and consider using database views, GraphQL DataLoader-style batching, or pagination to keep query scope tight. The goal is the right data in the fewest round-trips, not the fewest lines of ORM code.
© RM Full Stack & AI Engineer · All guides · Roadmaps · Open the app