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

Как исправить запрос

1.3 Junior🔥 101 комментариев
#Другое

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

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

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

# Как исправить запрос (оптимизация SQL)

Этот вопрос обычно касается исправления неэффективного или некорректного SQL запроса. Рассмотрю типичные проблемы и подходы к их решению.

Типичная проблема: N+1 Query Problem

Плохой запрос:

// Это генерирует 1 запрос для выборки пользователей + N запросов для каждого заказа
List<User> users = userRepository.findAll();
users.forEach(user -> {
    List<Order> orders = user.getOrders(); // Отдельный SQL запрос на каждого пользователя!
});

Исправленный запрос с JOIN FETCH:

// Генерирует 1 запрос с JOIN, загружает всё за раз
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders WHERE u.status = :status")
    List<User> findActiveUsersWithOrders(@Param("status") String status);
}

Проблема: SELECT * (вместо конкретных полей)

Плохо:

SELECT * FROM users;

Хорошо:

SELECT id, name, email, created_at FROM users;

В Hibernat/JPA:

@Query("""
    SELECT new map(u.id, u.name, u.email) 
    FROM User u 
    WHERE u.status = :status
""")
List<Map<String, Object>> getUserInfo(@Param("status") String status);

// или с DTO
@Query("""
    SELECT new com.example.dto.UserDTO(u.id, u.name, u.email)
    FROM User u
    WHERE u.status = :status
""")
List<UserDTO> getUserInfo(@Param("status") String status);

Проблема: Missing Indexes

Исходный медленный запрос:

SELECT * FROM orders WHERE user_id = 123 AND status = 'COMPLETED';

Исправление — добавить индекс:

CREATE INDEX idx_orders_user_status ON orders(user_id, status);

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

EXPLAIN ANALYZE 
SELECT * FROM orders WHERE user_id = 123 AND status = 'COMPLETED';

Проблема: Subquery вместо JOIN

Плохо (медленно):

SELECT * FROM users WHERE id IN (
    SELECT user_id FROM orders WHERE status = 'COMPLETED'
);

Хорошо (быстрее):

SELECT DISTINCT u.* FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'COMPLETED';

В JPA:

@Query("""
    SELECT DISTINCT u FROM User u
    INNER JOIN u.orders o
    WHERE o.status = 'COMPLETED'
""")
List<User> findUsersWithCompletedOrders();

Проблема: Missing WHERE clause

Опасно (выбирает всю таблицу):

List<Order> orders = orderRepository.findAll();

Правильно (с фильтром):

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByStatusAndCreatedAtAfter(
        OrderStatus status, 
        LocalDateTime date
    );
}

// или
@Query("""
    SELECT o FROM Order o 
    WHERE o.status = :status 
    AND o.createdAt >= :startDate
    ORDER BY o.createdAt DESC
    LIMIT :limit
""")
List<Order> findRecentOrders(
    @Param("status") OrderStatus status,
    @Param("startDate") LocalDateTime date,
    @Param("limit") int limit
);

Проблема: Неправильная агрегация

Плохо (несправедливое GROUP BY):

SELECT user_id, name, SUM(amount) 
FROM orders
GROUP BY user_id;
-- ERROR! 'name' не в GROUP BY

Правильно:

SELECT u.id, u.name, COUNT(o.id) as order_count, SUM(o.amount) as total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
HAVING SUM(o.amount) > 1000
ORDER BY total_amount DESC;

В JPA:

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("""
        SELECT new com.example.dto.UserOrderSummary(
            u.id, u.name, COUNT(o.id), SUM(o.amount)
        )
        FROM User u
        LEFT JOIN u.orders o
        GROUP BY u.id, u.name
        HAVING SUM(o.amount) > :minAmount
        ORDER BY SUM(o.amount) DESC
    """)
    List<UserOrderSummary> findUsersWithHighSpending(
        @Param("minAmount") BigDecimal minAmount
    );
}

Проблема: Cartesian Product (неправильный CROSS JOIN)

Плохо (генерирует огромное количество строк):

SELECT * FROM users, orders;
-- Если users=1000 строк, orders=10000 строк → 10 млн результатов!

Правильно:

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

Проблема: Функции в WHERE (ломает индекс)

Медленно:

SELECT * FROM orders WHERE YEAR(created_at) = 2024;
-- Не использует индекс на created_at

Быстрее:

SELECT * FROM orders 
WHERE created_at >= '2024-01-01' 
AND created_at < '2025-01-01';

В JPA:

@Query("""
    SELECT o FROM Order o
    WHERE o.createdAt >= :startOfYear
    AND o.createdAt < :endOfYear
""")
List<Order> findOrdersByYear(
    @Param("startOfYear") LocalDateTime start,
    @Param("endOfYear") LocalDateTime end
);

Проблема: Distinct на JOIN (замедляет)

Плохо:

@Query("""
    SELECT DISTINCT u FROM User u
    LEFT JOIN FETCH u.orders
    LEFT JOIN FETCH u.payments
""")
List<User> findAllUsers();
// DISTINCT работает в памяти, замедляет!

Лучше — использовать пагинацию:

@Query("""
    SELECT u FROM User u
    WHERE u.id IN (
        SELECT DISTINCT o.user.id FROM Order o
    )
""")
Page<User> findUsersWithOrders(Pageable pageable);

Утилиты для анализа

EXPLAIN ANALYZE:

EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM orders WHERE user_id = 123;

-- Результат покажет:
-- - время выполнения
-- - количество отсканированных строк
-- - используется ли индекс
-- - узкие места

В Java приложении:

@Configuration
public class JpaLoggingConfig {
    @Bean
    public PersistenceUnitManager persistenceUnitManager() {
        // Логировать все SQL запросы
        // logging.level.org.hibernate.SQL=DEBUG
        // logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
        return null;
    }
}

// application.properties
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type=TRACE

Чеклист оптимизации запросов

  1. ✅ Использовать индексы на часто фильтруемых полях
  2. ✅ Выбирать только нужные колонки (не SELECT *)
  3. ✅ Избегать N+1 (использовать JOIN FETCH или batch loading)
  4. ✅ Использовать пагинацию для больших результатов
  5. ✅ Проверять EXPLAIN ANALYZE перед production
  6. ✅ Избегать функций в WHERE clause
  7. ✅ Использовать LIMIT для тестирования
  8. ✅ Кэшировать часто читаемые данные
  9. ✅ Мониторить медленные запросы (query log)
  10. ✅ Регулярно анализировать и перестраивать индексы

Правильная оптимизация запросов часто даёт 10-100x улучшение производительности при минимальных затратах.

Как исправить запрос | PrepBro