Back to Blog
spring-boothibernatejpan-plus-oneperformancejava

How to Fix N+1 Query Problems in Spring Boot (2026 Guide)

N+1 queries silently kill your Spring Boot app performance. Learn how to detect and fix them with JOIN FETCH, @EntityGraph, and Hibernate batch fetching - with real code examples.

J

JOptimize Team

May 19, 2026· 8 min read

The N+1 query problem is one of the most common - and most damaging - performance issues in Spring Boot applications. What looks like a single database call silently becomes dozens or hundreds of queries at runtime, and by the time you notice, your app is already struggling under load.


What Is the N+1 Query Problem?

The N+1 problem occurs when your code executes 1 query to load a list of entities, then fires N additional queries to load a related association for each entity. If you have 100 orders and load their customers lazily, you get 1 + 100 = 101 queries instead of 1.

@Entity public class Order { @ManyToOne(fetch = FetchType.LAZY) private Customer customer; } // In your service: List<Order> orders = orderRepository.findAll(); // 1 query for (Order order : orders) { System.out.println(order.getCustomer().getName()); // N queries! }

With 100 orders, Hibernate silently fires 101 SQL statements. With 1000 orders, your database gets hammered with 1001 queries for what should be a single JOIN.


How to Detect N+1 Queries

The fastest way is to enable SQL logging and count the queries. Add this to application.properties:

spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true logging.level.org.hibernate.type.descriptor.sql=TRACE

For production-grade detection, enable Hibernate Statistics:

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

Check for HHH000117 in your logs - it shows the total query count per session. If you see 50 queries for what should be 1, you have an N+1 problem.


Solution 1: JOIN FETCH in JPQL

The most direct fix - tell Hibernate to load the association in a single JOIN query:

@Repository public interface OrderRepository extends JpaRepository<Order, Long> { @Query("SELECT o FROM Order o JOIN FETCH o.customer WHERE o.status = :status") List<Order> findByStatusWithCustomer(@Param("status") OrderStatus status); }

This generates a single SQL query with an INNER JOIN, loading both Order and Customer in one round-trip. Use LEFT JOIN FETCH if the association is optional.

Caveat: JOIN FETCH with pagination (Pageable) triggers a HHH90003004 warning and loads everything in memory. For paginated queries with associations, use @EntityGraph instead.


Solution 2: @EntityGraph

@EntityGraph is the Spring Data JPA-native way to eagerly load associations for a specific query without changing the entity's default fetch type:

@Repository public interface OrderRepository extends JpaRepository<Order, Long> { @EntityGraph(attributePaths = {"customer", "customer.address"}) List<Order> findByStatus(OrderStatus status); // Works correctly with pagination @EntityGraph(attributePaths = {"customer"}) Page<Order> findAll(Pageable pageable); }

For multiple collections, use named entity graphs to avoid MultipleBagFetchException:

@Entity @NamedEntityGraph( name = "Order.withItemsAndPayments", attributeNodes = { @NamedAttributeNode("items"), @NamedAttributeNode("payments") } ) public class Order { ... } // In repository: @EntityGraph("Order.withItemsAndPayments") List<Order> findByCustomerId(Long customerId);

Solution 3: Hibernate Batch Fetching

When you can't rewrite the query, batch fetching reduces N+1 to ceil(N/batchSize) + 1 queries:

# application.properties spring.jpa.properties.hibernate.default_batch_fetch_size=25

Or per-association:

@Entity public class Order { @ManyToOne(fetch = FetchType.LAZY) @BatchSize(size = 25) private Customer customer; }

With 100 orders and batch size 25, Hibernate fires 1 + 4 = 5 queries instead of 101. A quick win with zero query rewrites.


Common Mistakes to Avoid

  • Setting FetchType.EAGER globally - loads associations everywhere, even when you don't need them
  • JOIN FETCH without DISTINCT on collections - Hibernate may return duplicates; use SELECT DISTINCT o FROM Order o JOIN FETCH o.items
  • Fetching multiple collections with JOIN FETCH in one query - triggers MultipleBagFetchException; use @EntityGraph or two separate queries
  • Ignoring N+1 on write paths - N+1 inside a transaction on writes causes cascading SQL and can deadlock under load

Summary

The N+1 query problem happens when Hibernate lazily loads associations in a loop. Fix it with JOIN FETCH for simple cases, @EntityGraph for pagination or multiple paths, and Hibernate batch fetching as a quick fallback. Always verify the fix by checking your SQL logs - the query count should drop to 1 or 2.


Detect N+1 Automatically in Your Codebase

JOptimize statically analyzes your Spring Boot project and flags every repository method that will trigger an N+1 query at runtime - before you deploy. The IntelliJ plugin highlights the exact line and suggests the correct JOIN FETCH or @EntityGraph fix inline as you code.

Catch N+1 queries before they reach production - free scan, no configuration required.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.