Foundation · Build
The N+1 Query Problem
One query for the list plus one per row for each association is N+1; fetch joins, entity graphs, or batch sizing collapse it back to a handful of queries.
Primary source: Hibernate 6.6: Fetching Strategies and JOIN FETCH
Lesson 12 of 17
1d streakThe Architectural Question
LAZY defers cost to iteration time, so list screens pay per-row queries that unit tests with a single row never reveal.
What this guide proves
- N+1 Problem
- FetchType.LAZY
- JOIN FETCH
The N+1 query problem: Anatomy and latency explosion
The N+1 query problem is the single most prevalent performance killer in Spring Boot applications. It occurs when loading N parent entities results in 1 initial query followed by N individual queries to fetch an associated child relationship.
@Entity
public class Order {
@Id private Long id;
// LAZY loading: Items are NOT fetched in the initial SELECT
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items = new ArrayList<>();
}
// In Service:
List<Order> orders = orderRepository.findAll(); // Query 1: SELECT * FROM orders (returns 100 orders)
for (Order order : orders) {
// Queries 2..101: 100 SEPARATE SELECT queries executed in a loop!
System.out.println(order.getItems().size());
}// Database Log: 10:00:00.001 [main] DEBUG: SELECT o.* FROM orders o 10:00:00.012 [main] DEBUG: SELECT i.* FROM order_items i WHERE i.order_id = 1 10:00:00.015 [main] DEBUG: SELECT i.* FROM order_items i WHERE i.order_id = 2 ... (98 more queries!) ... Total queries: 101 queries for 100 rows! Latency: 450ms!
Resolving N+1 queries with @EntityGraph and JOIN FETCH
| Solution | Mechanism | SQL Generated | Best Use Case |
|---|---|---|---|
| @EntityGraph(attributePaths = {"items"}) | JPA 2.1 declarative fetch plan | Single query with LEFT OUTER JOIN | Spring Data derived queries & pagination. |
| JOIN FETCH in @Query | Explicit JPQL join fetch | Single query with INNER or LEFT JOIN | Custom repository query methods. |
public interface OrderRepository extends JpaRepository<Order, Long> {
// Solution 1: Declarative EntityGraph (1 Query with LEFT JOIN!)
@EntityGraph(attributePaths = {"items", "customer"})
List<Order> findAll();
// Solution 2: Explicit JPQL JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") OrderStatus status);
}Batch fetching tradeoffs and hibernate.default_batch_fetch_size
When joining multiple collections simultaneously produces a dangerous Cartesian product (MultipleBagFetchException), Hibernate's batch fetching optimization is the optimal solution.
# Configure Hibernate to batch lazy collections into SQL IN queries
spring.jpa.properties.hibernate.default_batch_fetch_size=25Instead of executing 100 separate queries, Hibernate groups uninitialized proxies into chunks of 25: SELECT * FROM order_items WHERE order_id IN (?, ?, ..., ?), reducing 101 queries down to 5 queries!
Check your understanding
Active Recall Checkpoint
What causes the N+1 query problem when iterating over a collection of entities?
Hint: How are lazy associations fetched?
Show answer & explanation
Loading N entities with 1 query, and then accessing a lazy association on each entity inside a loop, triggering N additional queries.
Active Recall Checkpoint
How does @EntityGraph solve the N+1 problem without changing the entity fetch type to EAGER globally?
Hint: What SQL clause does it inject?
Show answer & explanation
@EntityGraph overrides the fetch type for that specific query, generating a SQL LEFT OUTER JOIN to load the parent and children in a single round-trip.
Runtime Architecture & Flow
High-value visual model
N+1 Query Count
The expensive part is not the Java loop; it is the hidden database round trip triggered for every lazy association access.
1. Load parent page
One query loads N parent rows with lazy associations unresolved.
2. Iterate parents
Application code looks local but touches a lazy relationship.
3. N extra selects
Hibernate runs one additional query per parent row.
4. Use-case fetch plan
JOIN FETCH, EntityGraph, or DTO projection makes the query shape explicit.
Synthesis & Production Verification
Deliberate Failure Condition
- Break: Iterate author.getBooks() in the controller with open-in-view disabled
- Observe: LazyInitializationException because the Hibernate session already closed
- Repair: Load associations inside the transaction via JOIN FETCH or @EntityGraph; open-session-in-view masking is luck, not design.
Production Acceptance Criteria
- Count queries in the log and identify the 1+N pattern
- Fix it with JOIN FETCH and recount
- Explain how OSIV hides the exception in web threads only
Key Takeaway
The N+1 problem multiplies database round-trips when accessing lazy associations; resolve it with @EntityGraph, JOIN FETCH, and batch fetching.
Architectural & Technical Review
Core Interview Questions
Q01What causes the N+1 query problem in JPA applications?Show answer
Querying N parent records and then accessing lazy child associations inside a loop, triggering N additional queries.
Q02How do @EntityGraph and JOIN FETCH resolve N+1 queries?Show answer
They instruct Hibernate to fetch the parent and child associations together in a single SQL JOIN query.
Q03What is the purpose of hibernate.default_batch_fetch_size?Show answer
It instructs Hibernate to batch uninitialized lazy proxy loads into SQL IN queries, transforming N queries into N/batch queries.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Joining multiple collections in a single JOIN FETCH query — Fetching two distinct collections simultaneously produces a Cartesian product that causes MultipleBagFetchException.
- Pitfall: Relying on Open Session in View to mask N+1 queries — OSIV hides N+1 queries until production load exhausts the connection pool during JSON serialization.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources