Короткий ответ
Проблема N+1 — это 1 + N запросов вместо одного оптимального запроса с join: сначала выполняется один SELECT для получения N родительских записей, а затем для каждой из них — ещё по одному запросу за связанными данными. Чаще всего проявляется при ленивой загрузке ассоциаций, когда мы обходим список и у каждой сущности обращаемся к ленивой коллекции.
Как это работает подробнее
На небольших объёмах проблема незаметна, но с ростом данных производительность резко падает из-за множества обращений к базе. Все техники решения направлены на сокращение числа запросов, а выбор конкретного способа зависит от структуры запроса и сценария чтения.
- Fetch join: в HQL/JPQL связанные данные подгружаются в том же запросе через
JOIN FETCH. - @EntityGraph: в Spring Data JPA декларативно описывается граф загрузки — метод репозитория подтягивает указанные атрибуты одним запросом.
- Batch fetching:
@BatchSize(size = 10)на сущности или коллекции грузит связи пачками, сокращая число запросов с N до N/size. - EAGER: для связей, которые нужны всегда, можно осознанно выставить
FetchType.EAGER, но делать это следует осторожно.
Пример кода
Примеры решений N+1
// HQL/JPQL: fetch join подтягивает связи одним запросом
String hql = "SELECT d FROM Department d JOIN FETCH d.employees WHERE d.id = :id";
Department dept = session.createQuery(hql, Department.class)
.setParameter("id", 1L)
.uniqueResult();
// Spring Data JPA: декларативный граф загрузки @EntityGraph
@EntityGraph(attributePaths = {"employees"})
List<Department> findAll();Что использовать на практике
- Контролируйте реальное число запросов: включайте логирование SQL или статистику Hibernate.