JPA N+1 Problem: From Root Cause to Solutions
What Is the N+1 Problem?
When fetching a parent entity and then accessing its child entities, instead of one query, N+1 queries are executed.
1
2
3
4
5
List<Product> products = productRepository.findAll(); // 1 query
for (Product product : products) {
product.getOptions().size(); // N queries (once per product)
}
If there are 100 products, 101 queries run. This is the N+1 problem.
Why Does It Happen?
JPA @OneToMany and @ManyToOne use FetchType.LAZY by default. This is intentional - don’t load data until needed. But when you access associated entities inside a loop, queries end up firing N times.
Solution 1: Fetch Join
1
2
@Query("SELECT p FROM Product p JOIN FETCH p.options")
List<Product> findAllWithOptions();
Fetches everything in one query. But creates cartesian product, potentially duplicating parent rows.
Solution 2: @EntityGraph
1
2
3
@EntityGraph(attributePaths = {"options"})
@Query("SELECT p FROM Product p")
List<Product> findAllWithOptions();
Similar to Fetch Join, but cleaner separation between query and fetch strategy.
Solution 3: Batch Size
1
2
3
@BatchSize(size = 100)
@OneToMany(mappedBy = "product")
private List<Option> options;
Executes WHERE product_id IN (?, ?, ...) instead of individual queries. Not a single query, but significantly reduces query count.
Which to Choose?
| Solution | Pros | Cons |
|---|---|---|
| Fetch Join | Single query | Cartesian product, pagination issues |
| @EntityGraph | Clean code | Same limitations as Fetch Join |
| Batch Size | No cartesian product | Not a single query |
Use Fetch Join or EntityGraph when data is small. Use Batch Size when dealing with large datasets or pagination.
Lessons Learned
- N+1 is a common JPA pitfall, must understand it
- Pick the right solution for the situation
- Performance testing is necessary to verify solutions work
From Kakao Tech Campus 3rd cohort Gift API clone coding.