Post

JPA N+1 Problem: From Root Cause to Solutions

JPA N+1 Problem: From Root Cause to Solutions

What Is the N+1 Problem?

When you query entities with associations, you first run 1 query to fetch N entities—then N additional queries fire to load each entity’s related data.


Problem Situation

While migrating the Gift API from JDBC to JPA at Kakao Tech Campus, I hit the N+1 problem in the wishlist query feature.

Entity Structure

  • WishlistItem references Product via @ManyToOne(fetch = FetchType.LAZY)

Queries Fired

With 10 wishlist items:

  • 1 query: SELECT * FROM wishlist_item WHERE member_uuid = ?
  • 10 queries: SELECT * FROM product WHERE id = ? (one per item)

11 queries total.

np1.png


Comparing Solutions

1. Fetch Join

Use JOIN FETCH in JPQL to load related entities in one go.

1
2
@Query("SELECT w FROM WishlistItem w JOIN FETCH w.product WHERE w.member.uuid = :memberUuid")
List<WishlistItem> findByMemberUuidWithProduct(@Param("memberUuid") UUID memberUuid);
  • Pros: Done in 1 query
  • Cons: Can’t use with pagination; may cause cartesian product in many-to-many relations

2. EntityGraph

Use @EntityGraph(attributePaths = {"product"}) to specify what to fetch.

1
2
@EntityGraph(attributePaths = {"product"})
List<WishlistItem> findByMemberUuid(UUID memberUuid);
  • Pros: Works with method-name-based queries
  • Cons: Same limitations as Fetch Join

3. Batch Size

Set hibernate.default_batch_fetch_size to batch-load related entities using IN clauses.

1
spring.jpa.properties.hibernate.default_batch_fetch_size=100
  • Pros: Works with pagination
  • Cons: Query count is (N / batch_size) + 1, not truly a single query

Final Choice: EntityGraph

For this case, I went with EntityGraph.

Why:

  1. Wishlist queries return only one user’s items—not a lot of data
  2. Pagination wasn’t needed here
  3. I wanted to keep using Spring Data JPA’s method-name-based queries

Suitable Situations for Each Method

SituationRecommended
Associated entities without paginationFetch Join, EntityGraph
Pagination + associated entitiesBatch Size
Collection relationships (OneToMany)Batch Size
Single relationships (ManyToOne)Fetch Join, EntityGraph

Lessons Learned

  • Lazy Loading’s trap: FetchType.LAZY only delays N+1—it doesn’t solve it. Queries still fire when you access related entities.
  • Log your queries: Without SQL logs in dev, N+1 is easy to miss. Count your queries.
  • There’s no silver bullet: Fetch Join, EntityGraph, Batch Size each have trade-offs. Pick what fits.

From Kakao Tech Campus 3rd cohort Gift API clone coding (JDBC → JPA migration).

This post is licensed under CC BY 4.0 by the author.