Из-за чего в Hibernate возникает проблема N+1 select и как её убрать?
Один запрос грузит N родителей; обращение к ленивой связи каждого из них добавляет по одному SELECT на родителя — страница из 100 заказов выливается в 101 запрос. Убирается загрузкой связи тем же запросом — JOIN FETCH в JPQL или @EntityGraph на методе репозитория — либо пакетированием (@BatchSize), которое схлопывает N селектов в несколько запросов с IN (...). EAGER — не решение: он выпускает те же N селектов раньше.
- ✗Хвататься за
EAGERкак за решение — оно лишь переносит те же N селектов раньше - ✗Винить отсутствующий индекс, когда проблема в числе запросов, а не в их стоимости
- ✗Вовсе не замечать проблему, потому что она проявляется только на боевом N
- →Почему
@BatchSizeуменьшает число запросов, не соединяя таблицы? - →Что
JOIN FETCHделает с постраничностью черезsetMaxResults?
Где рождаются 1 + N запросов
@Entity
class Order {
@Id Long id;
@OneToMany(mappedBy = "order") // LAZY по умолчанию
List<OrderItem> items = new ArrayList<>();
}
// 1 запрос: SELECT * FROM orders LIMIT 100
List<Order> orders = em.createQuery("select o from Order o", Order.class)
.setMaxResults(100)
.getResultList();
for (Order o : orders) {
// ⚠️ на КАЖДОЙ итерации — ещё один SELECT по items этого заказа
total += o.getItems().size(); // 100 дополнительных запросов
}
Лог Hibernate выдаёт проблему сразу:
select o.id from orders o limit 100;
select i.* from order_items i where i.order_id = 1;
select i.* from order_items i where i.order_id = 2;
-- ... ещё 98 таких же
Три рабочих исправления
// 1 — JOIN FETCH: одна связь грузится тем же запросом
List<Order> orders = em.createQuery(
"select distinct o from Order o join fetch o.items", Order.class)
.getResultList(); // ровно 1 запрос
// 2 — @EntityGraph на методе репозитория Spring Data
public interface OrderRepo extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = "items")
List<Order> findAll();
}
// 3 — @BatchSize: N селектов схлопываются в N/размер запросов с IN (...)
@OneToMany(mappedBy = "order")
@BatchSize(size = 50)
List<OrderItem> items; // 100 заказов → 2 запроса вместо 100
❌ fetch = FetchType.EAGER не исправление: Hibernate по-прежнему выпустит те же N селектов, просто раньше — и теперь уже на каждый запрос, где сущность вообще упоминается.
⚠️ join fetch коллекции несовместим с setMaxResults: Hibernate вынужденно тащит все строки и режет их в памяти (предупреждение HHH000104). Для постраничности с коллекцией берите @BatchSize или два запроса — сначала id-шники страницы, затем where id in (...) с join fetch.