middle
Проблема N+1 запросов и способы решения
Проблема N+1 — ситуация, когда загрузка списка из N сущностей приводит к 1 основному запросу + N дополнительных запросов для загрузки связанных сущностей.
Пример проблемы
Пример
// 1 запрос для загрузки 100 пользователей
List<User> users = userRepository.findAll();
// SELECT * FROM users -- 1 запрос
// + 100 запросов для загрузки Department каждого пользователя
for (User user : users) {
System.out.println(user.getDepartment().getName());
// SELECT * FROM departments WHERE id = ? -- N запросов
}
// Итого: 1 + 100 = 101 запрос!
Решения
- JOIN FETCH (JPQL):
Пример
@Query("SELECT u FROM User u JOIN FETCH u.department")
List<User> findAllWithDepartment();
// Один запрос: SELECT u.*, d.* FROM users u JOIN departments d ON u.department_id = d.id
- @EntityGraph:
Пример
@EntityGraph(attributePaths = {"department", "orders"})
@Query("SELECT u FROM User u")
List<User> findAllWithGraph();
- @BatchSize (Hibernate):
Пример
@Entity
public class User {
@BatchSize(size = 25)
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
}
// Вместо 100 запросов — 4 запроса (по 25 ID):
// SELECT * FROM departments WHERE id IN (?, ?, ..., ?) — 4 раза
Глобальный @BatchSize:
Пример
spring.jpa.properties.hibernate.default_batch_fetch_size=25
- DTO-проекция (без загрузки сущностей):
Пример
@Query("SELECT new com.example.UserDto(u.name, d.name) " +
"FROM User u JOIN u.department d")
List<UserDto> findUserDtos();
// Один запрос, только нужные поля, без сущностей и прокси
Сравнение подходов
| Подход | Плюсы | Минусы |
|---|---|---|
| JOIN FETCH | Один запрос | Нельзя пагинировать коллекции; Cartesian product |
| EntityGraph | Декларативный, гибкий | Менее предсказуемый SQL |
| @BatchSize | Простой, не меняет запрос | Всё ещё несколько запросов |
| DTO-проекция | Максимальная эффективность | Нет управляемых сущностей |
Важное
- N+1 — самая частая проблема производительности Hibernate
- Всегда проверяйте SQL-логи при работе с коллекциями (
show-sqlилиlogging.level.org.hibernate.SQL=DEBUG) - JOIN FETCH — основное решение для
@ManyToOne; @BatchSize — для@OneToMany - Нельзя JOIN FETCH две коллекции одновременно → используйте @BatchSize для второй
Частые ошибки
- JOIN FETCH + пагинация —
findAll(Pageable)с JOIN FETCH выполнит пагинацию в памяти (загрузит все строки!); Hibernate выдаст предупреждение - JOIN FETCH для двух List-коллекций — MultipleBagFetchException; замените одну на Set
- Не замечать N+1 — приложение работает «нормально» на тестовых данных, падает в продакшене с тысячами записей
- Overfix — JOIN FETCH для всех связей приводит к огромным Cartesian product
Как используется в 2026
- Стандартный подход:
default_batch_fetch_size=25глобально + JOIN FETCH для критичных запросов - Spring Data JPA
@EntityGraph— удобный декларативный подход - Для read-heavy сценариев — DTO-проекции через Spring Data interface-based projections
- Hibernate 6
@FetchProfile— более гибкое управление стратегией загрузки
На собеседовании: N+1 — это вопрос-маркер: если кандидат не знает о нём, значит не работал с Hibernate на серьёзных проектах. Объясните проблему на примере (1 + N запросов), назовите решения: JOIN FETCH, @EntityGraph, @BatchSize, DTO-проекция. Упомяните, что нужно проверять SQL-логи.