Gymterview
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 запрос!

Решения

  1. 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
  1. @EntityGraph:
Пример
@EntityGraph(attributePaths = {"department", "orders"})
@Query("SELECT u FROM User u")
List<User> findAllWithGraph();
  1. @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
  1. 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-логи.