Что такое проблема N+1 запросов и как её решать?
- Почему Hibernate делает сотни запросов?
- Как найти проблему N+1?
N+1 — один запрос за списком и ещё по запросу на связанные данные каждого элемента: 100 заказов → 101 запрос. Причина — ленивая загрузка связей в цикле.
- Лечение: JOIN FETCH, @EntityGraph, пакетная загрузка (batch size), DTO-проекции
- Обнаружение: логи SQL, счётчик запросов в тестах, APM
- Каждый запрос — сетевой обход: 100 × 1 мс = 100 мс впустую
А ещё JOIN FETCH коллекций с пагинацией — ловушка: Hibernate пагинирует в памяти
Подробно
Механизм. ORM загружает список сущностей одним запросом, а связи (order.getCustomer(), order.getItems()) помечены ленивыми и подгружаются при первом обращении. Код проходит по списку в цикле — и на каждой итерации идёт отдельный запрос. В шаблоне или сериализаторе JSON это происходит незаметно.
Почему плохо: каждый запрос — обход по сети и разбор, даже быстрый; сотни запросов на страницу дают сотни миллисекунд и нагружают базу.
Как найти:
- включить лог SQL (
spring.jpa.show-sql, p6spy, datasource-proxy) и посмотреть на повторяющиеся запросы; - в тестах проверять число запросов (Hypersistence Utils, assertSelectCount);
- APM-трассировка показывает десятки одинаковых спанов.
Как лечить:
- JOIN FETCH в JPQL — связь загружается одним запросом с JOIN;
- @EntityGraph — описать, какие связи загрузить для конкретного запроса;
- пакетная загрузка —
hibernate.default_batch_fetch_size = 50: связи для 50 сущностей подгружаются одним запросом сIN (...); - DTO-проекции — выбрать ровно нужные поля одним запросом.
Ловушки: JOIN FETCH двух коллекций — декартово произведение (MultipleBagFetchException); JOIN FETCH коллекции с setMaxResults — Hibernate загрузит всё и пагинирует в памяти (предупреждение HHH000104).
Пример
// N+1: запрос за заказами + по запросу на клиента каждого заказа
for (Order o : orderRepo.findAll()) log.info(o.getCustomer().getName());
// один запрос
@Query("select o from Order o join fetch o.customer")
List<Order> findAllWithCustomer();Что хотят услышать
- Механизм через ленивую загрузку в цикле.
- Как обнаружить.
- Несколько способов исправления.
- Ловушки JOIN FETCH — плюс.
Частые ошибки
- Делать все связи EAGER — N+1 остаётся, а лишние данные грузятся всегда.
- Не проверять число запросов в тестах.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✗Запрос с N соединениями таблиц, который выполняется в N + 1 раз дольше
- ✗Ситуация, когда индекс по N колонкам требует N + 1 чтение страниц с диска при каждом поиске
- ✗Вставка N строк по одной вместо пакетной
- ✓Ленивая загрузка связей в цикле: один запрос за списком и ещё по запросу на каждый элемент
Что спросят дальше
- Чем @EntityGraph отличается от JOIN FETCH?
- Как поймать N+1 в тестах?
Разбор темы
- ORM, persistence context и N+1
Что делает ORM, lazy/eager и proxy, механизм N+1, способы лечения и типичные грабли.
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.