Back to Blog
spring-boothibernateperformancejpan+1

How to Detect and Fix N+1 Queries in Spring Boot

N+1 queries are one of the most common performance killers in Spring Boot applications. Learn how to detect them with code examples, and fix them permanently.

J

JOptimize Team

April 14, 2026· 8 min read

If your Spring Boot application feels slow under load, N+1 queries are likely the culprit. They're silent, hard to spot during development, and devastating in production — a single endpoint can fire hundreds of SQL queries without you realizing it.

In this article, we'll cover what N+1 queries are, how to detect them, and how to fix them permanently.

What is an N+1 Query Problem?

The N+1 query problem occurs when your application fires one query to fetch a list (the "1"), and then one additional query per item in that list (the "N") to fetch related data.

// This looks innocent... List<Order> orders = orderRepository.findAll(); // 1 query for (Order order : orders) { System.out.println(order.getUser().getEmail()); // N queries — one per order! }

If you have 500 orders, this code fires 501 SQL queries. Under load, this becomes catastrophic.

Why N+1 Happens in JPA / Hibernate

The root cause is lazy loading. By default, JPA loads associated entities only when you access them. When you iterate over a collection and access a lazy association, Hibernate fires a new query for each element.

@Entity public class Order { @ManyToOne(fetch = FetchType.LAZY) // lazy by default private User user; }

This is the correct default — you don't want to always load the user. But it becomes a problem when you access order.getUser() inside a loop.

How to Detect N+1 Queries

1. Enable SQL logging in development

Add this to your application.properties:

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

Then watch your console. If you see the same query repeated many times, you have an N+1 problem.

2. Use Hibernate Statistics

@Configuration public class HibernateConfig { @Bean public HibernatePropertiesCustomizer hibernatePropertiesCustomizer() { return props -> props.put("hibernate.generate_statistics", "true"); } }

Check SessionFactory.getStatistics() — look for getQueryExecutionCount().

3. Use JOptimize static analysis

JOptimize detects N+1 patterns automatically in your codebase — no runtime needed:

joptimize analyze . # → [CRITICAL] N+1 query detected: orderRepository.findAll() result iterated # with lazy association access — OrderService.java:45

4. Use JOptimize dynamic monitoring

The JVM agent confirms N+1 at runtime — it counts actual SQL queries per method:

joptimize monitor --app "java -jar myapp.jar" # → getOrderSummary(): 247 SQL queries in 1 invocation — N+1 confirmed

How to Fix N+1 Queries

Fix 1: JOIN FETCH in JPQL

The most direct fix — load everything in a single query:

@Query("SELECT o FROM Order o JOIN FETCH o.user WHERE o.status = :status") List<Order> findByStatusWithUser(@Param("status") String status);

This generates one SQL JOIN instead of N+1 selects.

Fix 2: @EntityGraph

Cleaner syntax, same result:

@EntityGraph(attributePaths = {"user", "items"}) List<Order> findByStatus(String status);

Use this when you need to conditionally load associations without writing JPQL.

Fix 3: Batch fetching with @BatchSize

When you can't always use JOIN FETCH, tell Hibernate to batch the secondary queries:

@Entity public class Order { @ManyToOne(fetch = FetchType.LAZY) @BatchSize(size = 50) private User user; }

Instead of N queries, Hibernate fires N/50 queries using IN clauses. Not perfect, but much better.

Fix 4: Load data before the loop

Sometimes the simplest fix is to pre-fetch IDs and load in bulk:

// Before — N+1 for (Long userId : userIds) { User user = userRepository.findById(userId).orElseThrow(); // N queries process(user); } // After — 1 query List<User> users = userRepository.findAllById(userIds); // 1 IN clause for (User user : users) { process(user); }

Fix 5: DTO projections with @Query

When you only need specific fields, skip entity loading entirely:

public interface OrderSummary { String getOrderId(); String getUserEmail(); Double getAmount(); } @Query("SELECT o.id as orderId, o.user.email as userEmail, o.amount as amount FROM Order o") List<OrderSummary> findOrderSummaries();

This avoids loading full entities and all their associations.

The EAGER Anti-Pattern

A common "fix" that actually makes things worse:

@OneToMany(fetch = FetchType.EAGER) // ❌ Don't do this private List<OrderItem> items;

EAGER loading solves N+1 by always loading everything — but this means every query loads all associations, even when you don't need them. It trades N+1 for always-too-much data.

JOptimize flags FetchType.EAGER on collections as a CRITICAL issue for this reason.

Quick Reference

  • Lazy association accessed in loop → JOIN FETCH or @EntityGraph
  • Need conditional loading → @EntityGraph on the repository method
  • Legacy code, can't refactor easily → @BatchSize
  • Loading by list of IDs → findAllById()
  • Need only specific fields → DTO projection with @Query
  • EAGER on collections → Switch to LAZY + explicit fetch when needed

Detect N+1 in Your Project Now

Run JOptimize on your codebase to find all N+1 patterns, EAGER collections, and other Hibernate anti-patterns in seconds:

npm install -g @joptimize/cli joptimize auth jp_live_your_key joptimize analyze .

Or upload your ZIP directly at joptimize.io.

Want to go deeper?

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

Detect N+1 queries in your project

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