N+1 Query Problem — Practical Demo
Hands-on examples for The N+1 Query Problem. We use
Order/Customer/OrderItemto show the problem and all practical fixes.
You should understand JPA Basics (LAZY/EAGER fetch types) and Spring Data Repositories. See The N+1 Query Problem for full theory.
Example 1: Reproduce the N+1 Problem
Enable SQL logging and observe individual queries being fired per customer access.
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // ← LAZY: customer not loaded with order
@JoinColumn(name = "customer_id")
private Customer customer;
private OrderStatus status;
}
@Transactional(readOnly = true)
public void printAllOrders() {
List<Order> orders = orderRepo.findAll(); // ← Query 1: SELECT * FROM orders
for (Order order : orders) {
// Accessing customer triggers a separate SELECT for each order
System.out.println(order.getCustomer().getName()); // ← Query 2…N+1: per order!
}
}
SQL log output (with 3 orders):
select * from orders
select * from customers where id = 1
select * from customers where id = 2
select * from customers where id = 3
-- Total: 4 queries for 3 orders. With 1000 orders → 1001 queries.
You won't notice this in development with 5 rows. It destroys performance in production with thousands of records.
Example 2: Fix with JOIN FETCH
Replace findAll() with a custom query that joins the association eagerly.
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.customer") // ← single JOIN query
List<Order> findAllWithCustomers();
}
@Transactional(readOnly = true)
public void printAllOrders() {
List<Order> orders = orderRepo.findAllWithCustomers(); // ← single query with JOIN
for (Order order : orders) {
System.out.println(order.getCustomer().getName()); // ← no extra SQL: already loaded
}
}
SQL log output:
select o.*, c.*
from orders o
inner join customers c on o.customer_id = c.id
-- Total: 1 query, regardless of number of orders.
Example 3: Fix with @EntityGraph
@EntityGraph is cleaner for derived query methods — no custom JPQL needed.
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = {"customer"}) // ← fetch customer alongside order
List<Order> findByStatus(OrderStatus status); // ← derived query + EntityGraph
}
List<Order> pendingOrders = orderRepo.findByStatus(OrderStatus.PENDING);
// SQL: SELECT o.*, c.* FROM orders o LEFT JOIN customers c ON o.customer_id = c.id WHERE o.status = 'PENDING'
// customer is already populated — no N+1
pendingOrders.forEach(o -> System.out.println(o.getCustomer().getName()));
Example 4: Fix Collection N+1 with @BatchSize
For @OneToMany (items in an order), use @BatchSize to batch the lazy collection loads.
@Entity
public class Order {
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
@BatchSize(size = 25) // ← Hibernate will batch-load up to 25 collections per query
private List<OrderItem> items = new ArrayList<>();
}
@Transactional(readOnly = true)
public void printOrderItems() {
List<Order> orders = orderRepo.findAll(); // ← loads 100 orders: 1 query
for (Order order : orders) {
List<OrderItem> items = order.getItems(); // ← batched: Hibernate groups 25 orders at a time
System.out.println("Order " + order.getId() + " has " + items.size() + " items");
}
}
SQL log output (100 orders):
select * from orders -- 1 query
select * from order_items where order_id in (1,2,...,25) -- batch 1
select * from order_items where order_id in (26,27,...,50) -- batch 2
select * from order_items where order_id in (51,52,...,75) -- batch 3
select * from order_items where order_id in (76,77,...,100) -- batch 4
-- Total: 5 queries instead of 101
@BatchSize is a low-effort safety net — add hibernate.default_batch_fetch_size=25 globally in application.yml and every @OneToMany and @ManyToMany benefits without any code annotation change.
Exercises
- Easy: Enable SQL logging, create 5
Orderrecords each with a differentCustomer, and callfindAll()in a@Transactionalmethod. Count how many SQL statements appear in the log. Then switch tofindAllWithCustomers()and count again. - Medium: Add
@OneToMany private List<OrderItem> itemstoOrder(without@BatchSize). Load 10 orders and accessitemsfor each. Count queries. Then add@BatchSize(size = 5)and count again. - Hard: Write a
@DataJpaTestintegration test using Hypersistence UtilsSQLStatementCountValidatorthat assertsfindAllWithCustomers()issues exactly 1 SELECT statement.
Back to Topic
Return to The N+1 Query Problem for theory, fix comparison table, interview questions, and further reading.