N+1 detection & fixes
On this page
What Is N+1 and Why It Destroys Production
N+1 happens when: - One query loads parent entities - Then N additional queries load related entities Example: Load 100 users. For each user, load orders separately. Total queries = 101. Under load, this becomes: - Latency spike - DB CPU explosion - Thread pool exhaustion This does not show in small datasets. It explodes in production.Incident Scenario: Latency Jump from 40ms to 900ms
Endpoint: GET /users Everything fine in staging (20 users). Production had 5,000 users. Root cause: Each user triggered lazy load of roles. 5,001 queries per request. Database survived. Application threads did not.Anti-Pattern: Lazy Collection Access in Loop
ListThis triggers: 1 query for users N queries for orders Hidden cost. Silent disaster.users = userRepository.findAll(); for (User u : users) { System.out.println(u.getOrders().size()); }
How to Detect N+1
1. Enable SQL logging in staging:spring.jpa.show-sql=true hibernate.format_sql=trueIf you see repeating queries per row → N+1. 2. Enable Hibernate statistics:
hibernate.generate_statistics=trueMonitor: - Query count per request - Entity fetch count If query count scales with result size → problem. 3. Use APM tools: - New Relic - Datadog - OpenTelemetry traces Look for repetitive SQL spans.
Fix Strategy 1: Fetch Join
Explicitly load relations:
@Query("SELECT u FROM User u JOIN FETCH u.orders")
List findAllWithOrders();
Now:
- Single SQL
- Proper join
- Predictable performance
Be careful:
Fetch join with pagination is dangerous.
Database may multiply rows.
Fix Strategy 2: Batch Fetching
Instead of 1 + N queries, Hibernate can group loads. Set batch size:hibernate.default_batch_fetch_size=50Now N queries become: ceil(N / 50) This reduces load significantly.
Fix Strategy 3: Projection Instead of Entity
Often you do not need full entity. Use projection:
@Query("SELECT new com.example.UserSummary(u.id, u.email) FROM User u")
List findSummaries();
This:
- Avoids relationship traversal
- Avoids lazy loading entirely
- Reduces memory footprint
Best for read-heavy endpoints.