← Назад к вопросам

Сколько сгенерируется запросов при поиске в связке двух таблиц?

2.0 Middle🔥 151 комментариев
#Другое

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

N+1 Query Problem в Hibernate/JPA

"Сколько сгенерируется запросов при поиске в связке двух таблиц?" — это классический N+1 Query Problem.

Проблема N+1

Пример: найти всех пользователей и их заказы

@Entity
@Table(name = "users")
public class User {
    @Id
    private UUID id;
    
    private String name;
    
    @OneToMany(mappedBy = "user")
    private List<Order> orders; // Связь с заказами
}

@Entity
@Table(name = "orders")
public class Order {
    @Id
    private UUID id;
    
    private double amount;
    
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

ПЛОХО: N+1 запросы

@Service
public class UserService {
    private final UserRepository userRepository;
    
    public void loadUsersAndOrders() {
        // Запрос 1: SELECT * FROM users
        List<User> users = userRepository.findAll();
        
        // Запросы 2, 3, 4, ... N+1: для каждого пользователя
        for (User user : users) {
            // Каждый вызов .getOrders() генерирует новый запрос!
            List<Order> orders = user.getOrders();
            System.out.println(user.getName() + " has " + orders.size() + " orders");
        }
        
        // Если пользователей 1000, будет:
        // 1 + 1000 = 1001 запрос! ❌
    }
}

SQL генерируется так:

-- Запрос 1
SELECT * FROM users;

-- Запросы 2-1001 (по одному на каждого пользователя)
SELECT * FROM orders WHERE user_id = '...';
SELECT * FROM orders WHERE user_id = '...';
SELECT * FROM orders WHERE user_id = '...';
-- ... и так 1000 раз

Решение 1: Eager Loading (LEFT JOIN FETCH)

@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
    @Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders")
    List<User> findAllWithOrders();
}

@Service
public class UserService {
    public void loadUsersAndOrders() {
        // 1 запрос с JOIN
        List<User> users = userRepository.findAllWithOrders();
        
        for (User user : users) {
            // Данные уже загружены, нет доп запросов
            List<Order> orders = user.getOrders();
            System.out.println(user.getName() + " has " + orders.size() + " orders");
        }
        // Всего запросов: 1 ✓
    }
}

SQL:

SELECT DISTINCT u.* FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

-- Один запрос возвращает:
-- user_id | name     | order_id | amount
-- 1       | John     | 101      | 100
-- 1       | John     | 102      | 200
-- 2       | Jane     | 103      | 150

Решение 2: EntityGraph

@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
    @EntityGraph(attributePaths = {"orders"})
    List<User> findAll();
}

// Использование
List<User> users = userRepository.findAll();
// 1 запрос с LEFT JOIN FETCH

Решение 3: Batch Fetching

# application.yml
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 20
@Entity
public class User {
    @OneToMany(mappedBy = "user")
    @BatchSize(size = 20)
    private List<Order> orders;
}

SQL:

SELECT * FROM users;  -- 1 запрос
SELECT * FROM orders WHERE user_id IN (...20 users);  -- 1 запрос
SELECT * FROM orders WHERE user_id IN (...20 users);  -- 1 запрос (для следующей партии)

-- Для 1000 пользователей: 1 + (1000/20) = 51 запрос

Решение 4: DTO Projection (лучший вариант)

public record UserOrderDTO(
    UUID userId,
    String userName,
    UUID orderId,
    double amount
) {}

@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
    @Query("""n        SELECT new com.example.UserOrderDTO(
            u.id, u.name, o.id, o.amount
        )
        FROM User u
        LEFT JOIN u.orders o
        """)
    List<UserOrderDTO> findUsersWithOrders();
}

@Service
public class UserService {
    public void loadUsersAndOrders() {
        // 1 запрос, только нужные данные
        List<UserOrderDTO> results = userRepository.findUsersWithOrders();
        
        for (UserOrderDTO dto : results) {
            System.out.println(dto.userName() + " - " + dto.amount());
        }
        // Запросов: 1
        // Памяти: минимум (только DTO, не загружаются сущности)
    }
}

Сравнение подходов

┌─────────────────────┬──────────┬────────┬──────────┬─────────────┐
│ Подход              │ Запросы  │ Память │ Гибкость │ Рекомендуется│
├─────────────────────┼──────────┼────────┼──────────┼─────────────┤
│ N+1 (плохо)         │ N+1 ❌   │ Высокая│ Низкая   │ НИКОГДА     │
│ LEFT JOIN FETCH     │ 1        │ Высокая│ Средняя  │ Когда нужны │
│ EntityGraph         │ 1        │ Высокая│ Средняя  │ Альтернатива│
│ Batch Fetching      │ N/20     │ Средняя│ Средняя  │ Хорошо      │
│ DTO Projection      │ 1        │ Низкая │ Высокая  │ ЛУЧШИЙ ✓    │
└─────────────────────┴──────────┴────────┴──────────┴─────────────┘

Включи SQL логирование для отладки

# application.yml
logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

spring:
  jpa:
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true

Вывод лога:

[SQL]
select distinct u1_0.id, u1_0.name 
from users u1_0 
left join orders o1_0 on u1_0.id=o1_0.user_id

[SQL]
select o1_0.user_id,o1_0.id,o1_0.amount 
from orders o1_0 
where o1_0.user_id in (?,?,?,?,?,?)

На собеседовании

Правильный ответ:

"Без оптимизации будет N+1 запросов, где N — количество пользователей. Первый запрос получает всех пользователей, затем для каждого пользователя — ещё один запрос для его заказов.

Это можно оптимизировать с помощью:

  1. LEFT JOIN FETCH — один запрос с JOIN
  2. EntityGraph — декларативно указать какие связи загрузить
  3. Batch Fetching — загружать партиями (более эффективно чем N+1)
  4. DTO Projection — вернуть только нужные поля (самый эффективный)"

Ключевые выводы

  • Проблема N+1 — частая в Hibernate приложениях
  • Всегда логируй SQL — сразу видна проблема
  • Используй JOIN FETCH или EntityGraph для связей
  • DTO Projection лучше когда нужны только некоторые поля
  • Batch Fetching хороший компромисс между простотой и производительностью
  • На prod без такой оптимизации — 500 ошибок, потому что N+1 запросов затопит БД
Сколько сгенерируется запросов при поиске в связке двух таблиц? | PrepBro