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

25 min

Lesson 12 of 17

1d streak

The 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.

OrderQueries.java
@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()); 
}
Terminal / Diagnostic Output
// 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

SolutionMechanismSQL GeneratedBest Use Case
@EntityGraph(attributePaths = {"items"})JPA 2.1 declarative fetch planSingle query with LEFT OUTER JOINSpring Data derived queries & pagination.
JOIN FETCH in @QueryExplicit JPQL join fetchSingle query with INNER or LEFT JOINCustom repository query methods.
OptimizedOrderRepository.java
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.

application.properties
# Configure Hibernate to batch lazy collections into SQL IN queries
spring.jpa.properties.hibernate.default_batch_fetch_size=25

Instead 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.

Query-count comparison: Fix the fetch plan at the query boundary instead of changing every association to EAGER.

Synthesis & Production Verification

Deliberate Failure Condition

  1. Break: Iterate author.getBooks() in the controller with open-in-view disabled
  2. Observe: LazyInitializationException because the Hibernate session already closed
  3. 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

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