Lesson 14 / 25
The N+1 Select Problem
Detect and fix N+1 queries with join fetch, entity graphs and batch fetching.
One query became a hundred
The N+1 select problem is the most common ORM performance bug. You load N orders with one query, then loop over them and access a lazy association such as order.getCustomer().getName(); Hibernate issues one additional query per order, so 1 + N queries instead of one or two. With 1,000 orders, that is 1,001 round trips. It hides easily: each query is fast, but together they dominate response time. Detect it by logging SQL in development, enabling Hibernate statistics, or using tools that count queries per request (some teams assert query counts in tests). Fix it with: join fetch in JPQL (select o from Order o join fetch o.customer), which loads associations in the same query; entity graphs (@EntityGraph in Spring Data, or jakarta.persistence.fetchgraph hints) to choose associations per use case; batch fetching (@BatchSize or hibernate.default_batch_fetch_size), which loads lazy associations for many owners at once with IN (...) queries; or DTO projections that select exactly the columns needed. Beware of join fetch on collections combined with pagination, which Hibernate cannot apply in SQL and may do in memory.
From N+1 to two queries
Join fetch for single-valued associations; batch fetching for collections.
// N+1: 1 query for orders, then 1 per order for its customer
List<PurchaseOrder> orders = em.createQuery("select o from PurchaseOrder o", PurchaseOrder.class)
.getResultList();
for (PurchaseOrder o : orders) {
log(o.getCustomer().getName()); // triggers a SELECT each time
}
// fix 1: join fetch the to-one association
List<PurchaseOrder> fixed = em.createQuery(
"select o from PurchaseOrder o join fetch o.customer where o.placedAt >= :since",
PurchaseOrder.class)
.setParameter("since", since)
.getResultList();
// fix 2: Spring Data entity graph per repository method
public interface OrderRepository extends JpaRepository<PurchaseOrder, Long> {
@EntityGraph(attributePaths = {"customer"})
List<PurchaseOrder> findByPlacedAtAfter(Instant since);
}
// fix 3: batch fetching for lazy collections (application.yml)
// spring.jpa.properties.hibernate.default_batch_fetch_size: 50
// -> lines of 50 orders loaded with one SELECT ... WHERE order_id IN (?, ?, ...)Fetching groceries one item per trip
N+1 is driving to the shop once for the list, then making a separate trip for every item on it. Join fetching or batch fetching is buying everything on one or two trips.
Quick check: You load 200 orders and print each order's customer name, and the SQL log shows 201 queries. What is the most direct fix?
- Switch every association to EAGER
- Increase the connection pool size
- Disable SQL logging
- Use join fetch (or an entity graph) for the customer association in that query
Answer
Use join fetch (or an entity graph) for the customer association in that query — Fetching the association in the same query removes the per-row queries.