TestForge | Aidevops | 📊 Plogger ✍️ Blog 📚 Docs
plogger

AI DevOps Korea

Turn AI service development and operations into one improvement loop

Aidevops.kr covers LLMOps, RAG, agents, observability, evaluation, and cost-performance optimization for production AI services.

A Complete Guide to Solving the JPA N+1 Problem

A Complete Guide to Solving the JPA N+1 Problem diagram
Visual guide to the key flow, architecture, and decision points covered in this post.
## Structure at a Glance
[Load Orders]
     |
     +--> 1 query: select * from orders
     |
     +--> N queries: select * from member where id = ?
                     select * from member where id = ?
                     select * from member where id = ?

The N+1 problem is not just “too many queries.” It is a structural issue where one screen-level read operation gets amplified into extra queries inside a loop. It may look harmless at first, but as data volume grows, both network round-trips and DB load increase, so the key is to understand this “1 + N” flow visually first.

What Is the N+1 Problem?

It happens when one query fetches N results, and then N additional queries are executed, one for each result.

// Query 10 posts -> 1 query
List<Post> posts = postRepository.findAll();

// Load comments for each post -> 10 extra queries!
posts.forEach(p -> System.out.println(p.getComments().size()));
// 11 queries total

Solution 1 — Fetch Join

@Query("SELECT DISTINCT p FROM Post p JOIN FETCH p.comments WHERE p.id IN :ids")
List<Post> findAllWithComments(@Param("ids") List<Long> ids);

Drawback: Pagination is not supported for collection fetch joins.

Solution 2 — @EntityGraph

@EntityGraph(attributePaths = {"comments", "author"})
@Query("SELECT p FROM Post p")
List<Post> findAllWithGraph();
# application.yml - global setting
spring.jpa.properties.hibernate.default_batch_fetch_size: 100
@BatchSize(size = 100)
@OneToMany(mappedBy = "post")
private List<Comment> comments;

Fetch N rows at once with an IN query -> SELECT * FROM comments WHERE post_id IN (1,2,...,100)

Solution 4 — Direct DTO Query

@Query("""
    SELECT new com.example.dto.PostSummary(p.id, p.title, COUNT(c))
    FROM Post p LEFT JOIN p.comments c
    GROUP BY p.id, p.title
    """)
List<PostSummary> findPostSummaries();

How to Choose a Solution

SituationRecommended Approach
Single-item readFetch Join
List + paginationBatch Size
Complex aggregationDirect DTO query
Multiple collectionsBatch Size

Counting Queries with Hibernate Statistics

spring.jpa.properties.hibernate.generate_statistics: true
logging.level.org.hibernate.stat: DEBUG

What Gets Hard in Production

  • N+1 issues are rarely just an ORM bug. They usually reveal that read-model design and fetch boundaries were never made explicit.
  • A system can pass tests and still collapse under production data volume when lazy access patterns multiply.
  • The cost is not only latency but also unpredictable database pressure.

Architecture Decisions That Matter

  • Design query use cases explicitly instead of assuming entity navigation is a free read model.
  • Use fetch join, entity graph, projection, or dedicated query models based on the screen requirement.
  • Separate write-oriented aggregate models from read-oriented query paths where necessary.

Practical Example

A query should reflect the exact screen need rather than generic entity traversal:

select o from Order o
join fetch o.customer
where o.status = :status

Anti-Patterns to Avoid

  • Rendering lists by traversing lazy relations inside loops.
  • Assuming EAGER fixes design problems instead of moving them.
  • Measuring only single-request success and not total query count.

Operational Checklist

  • Capture SQL count in integration tests for critical endpoints.
  • Inspect slow endpoints with real production-like data volume.
  • Prefer projections for read-heavy list screens.
  • Keep ORM convenience subordinate to database access discipline.

Final Judgment

N+1 is best solved by better query design, not by toggling fetch options blindly. The real fix is to make read intent explicit.

Continue Reading

Related posts

Next Path

Keep exploring this topic as a system