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.
JOptimize Team
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.
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.
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.
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.
@Configuration public class HibernateConfig { @Bean public HibernatePropertiesCustomizer hibernatePropertiesCustomizer() { return props -> props.put("hibernate.generate_statistics", "true"); } }
Check SessionFactory.getStatistics() — look for getQueryExecutionCount().
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
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
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.
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.
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.
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); }
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.
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.
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.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.