The N+1 query problem is the bug that doesn't look like a bug. With 5 rows in dev it's instant; in production with 500 rows it's 501 queries and a page that takes seconds. It never throws, so it hides until it hurts. So I built a visualizer that shows the query log filling one line per row — and then collapsing when you fix it.
▶ Live demo: https://dev48v.github.io/n-plus-one-visualizer/
Source (single file, zero deps): https://github.com/dev48v/n-plus-one-visualizer
What N+1 actually is
Load a list, then touch a lazy association on each element:
List<Order> orders = orderRepo.findAll(); // 1 query
for (Order o : orders) {
System.out.println(o.getCustomer().getName()); // ← fires a query PER order
}
select * from orders; -- 1
select * from customer where id = ?; -- then ONE per order → N
That's 1 + N queries for a single page. The demo runs it with a live counter: N=10 → 11 queries, N=100 → 101. Each one is a network round-trip to the database.
The fixes, side by side
- JOIN FETCH — one query, the association eagerly joined:
@Query("select o from Order o join fetch o.customer")
List<Order> findAllWithCustomer();
- @EntityGraph — the same single JOIN, declaratively, no JPQL:
@EntityGraph(attributePaths = "customer")
List<Order> findAll();
- Batch fetching — keep lazy, but group the child selects:
@BatchSize(size = 5) // or hibernate.default_batch_fetch_size=5
select * from customer where id in (?,?,?,?,?); -- ⌈N/5⌉ batched selects
The demo's "vs lazy" stat shows the savings: JOIN/EntityGraph → 1 query; batch → 1 + ⌈N/5⌉.
The trade-off nobody mentions
JOIN FETCH across two collections causes a cartesian product — rows multiply, and it gets slower, not faster. That's exactly when batch fetching wins. And join fetch + pagination (setMaxResults) makes Hibernate paginate in memory with a warning. There's no single right answer — which is why seeing the query counts for each strategy beats memorizing a rule.
Catch it in your own app
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.generate_statistics=true
If one endpoint logs dozens of near-identical select ... where id=? lines, that's your N+1.
If this saved you from an N+1 in prod, a star helps others find it: https://github.com/dev48v/n-plus-one-visualizer
Top comments (0)