Stage 9: JDBC, JPA and Hibernate, lesson 5 of 6

The N+1 problem (and how to fix it)

Advanced3 min read@since 17Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

N+1 happens when you load N parent rows with one query, and then touching a lazy association fires one more query per row.

It's the most common JPA performance bug, and it hides in loops, toString(), JSON serialization and templates.

Fixes, roughly in order of preference:

  • JOIN FETCH in JPQL: select c from Course c join fetch c.author
  • @EntityGraph on a Spring Data method
  • DTO projections that select exactly the columns you need
  • Batch fetching: @BatchSize(size = 50) or hibernate.default_batch_fetch_size loads lazy data in groups using IN (...)

To detect it, turn on SQL logging in development or assert query counts in tests.

Example

Java
// N+1: one query for courses, then one per course for its author
List<Course> courses = courseRepository.findAll();
courses.forEach(c -> System.out.println(c.getAuthor().getName()));

// Fix 1: fetch join
@Query("select c from Course c join fetch c.author where c.published = true")
List<Course> findPublishedWithAuthor();

// Fix 2: entity graph
@EntityGraph(attributePaths = {"author", "tags"})
List<Course> findByLevel(Level level);

// Fix 3: DTO projection: one query, only the columns you need
record CourseCard(String title, String authorName) {}

@Query("select new com.javaatlas.CourseCard(c.title, a.name) from Course c join c.author a")
List<CourseCard> findCards();

Common mistake

Switching associations to EAGER to "fix" LazyInitializationException. EAGER doesn't remove N+1; it makes it happen on every query, even where you don't need the data.

Under the hood

Fetch-joining two collections at once causes a cartesian product of rows, and Hibernate refuses two List bags with MultipleBagFetchException; fetch one collection per query or use batch fetching. A fetch join combined with pagination makes Hibernate paginate in memory (it logs a warning); page the ids first, then fetch by those ids. Set spring.jpa.properties.hibernate.default_batch_fetch_size=50 as a safety net.

Check yourself

Which is NOT a fix for N+1?

How this connects

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Part of Job-ready backend developer, Crack the Java interview.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.