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

Что делать если запрос стал отрабатывать менее эффективно

1.7 Middle🔥 231 комментариев
#REST API и микросервисы#Базы данных и SQL

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

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

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

# Диагностика и оптимизация неэффективных запросов

Когда запрос, который работал быстро, внезапно замедляется — это признак проблемы в коде, БД или инфраструктуре. Рассмотрю методическое решение этой проблемы.

Шаг 1: Проверить логи и мониторинг

// Добавляем логирование времени выполнения
@Service
public class UserService {
    @Autowired
    private UserRepository repository;
    
    @Autowired
    private LogService logService;
    
    public List<User> getAllUsers() {
        long startTime = System.currentTimeMillis();
        
        try {
            List<User> users = repository.findAll();
            long duration = System.currentTimeMillis() - startTime;
            
            logService.log("getAllUsers took " + duration + "ms");
            
            if (duration > 1000) { // Если больше секунды
                logService.warn("getAllUsers is slow: " + duration + "ms");
            }
            
            return users;
        } catch (Exception e) {
            logService.error("Error in getAllUsers", e);
            throw e;
        }
    }
}

Шаг 2: Профилировать запрос

На уровне БД

-- PostgreSQL: EXPLAIN ANALYZE
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 123;

-- Результат показывает:
-- Seq Scan on orders  (sequential scan = без индекса)
-- Rows: 10000 (сканирует все 10000 строк)
-- Planning time: 0.2ms
-- Execution time: 45.2ms

-- Решение: создать индекс
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

-- Повторный запрос:
-- Index Scan using idx_orders_customer_id
-- Rows: 50
-- Execution time: 0.8ms (55x быстрее!)

На уровне JPA/Hibernate

// Включаем SQL логирование
// 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.descriptor.sql.BasicBinder=TRACE

// Пример вывода:
// SELECT u.* FROM users u WHERE u.id = ?
// Binding: [1]

// Или используем Hibernate statistics
@Configuration
public class HibernateConfig {
    @Bean
    public StatisticsFilter statisticsFilter() {
        return new StatisticsFilter();
    }
}

Шаг 3: Распространённые причины деградации

Причина 1: N+1 запросы

// ПЛОХО: вызовет N+1 запрос
@Entity
public class Customer {
    @OneToMany
    private List<Order> orders; // Без FetchType.LAZY
}

// Код:
List<Customer> customers = repository.findAll();
for (Customer c : customers) {
    System.out.println(c.getOrders()); // Каждая итерация = отдельный SQL!
    // 1 запрос findAll() + N запросов для каждого getOrders()
}

// ХОРОШО: FetchType.LAZY + явная загрузка
@Entity
public class Customer {
    @OneToMany(fetch = FetchType.LAZY)
    private List<Order> orders;
}

// Или с JOIN FETCH
public interface CustomerRepository extends JpaRepository<Customer, Long> {
    @Query("SELECT DISTINCT c FROM Customer c LEFT JOIN FETCH c.orders")
    List<Customer> findAllWithOrders();
}

// Или с @EntityGraph
public interface CustomerRepository extends JpaRepository<Customer, Long> {
    @EntityGraph(attributePaths = "orders")
    List<Customer> findAll();
}

Причина 2: Отсутствие индексов

-- До оптимизации
SELECT * FROM transactions 
WHERE created_at > '2024-01-01' AND status = 'pending';
-- Time: 3000ms (сканирует 1 млн строк)

-- После оптимизации
CREATE INDEX idx_transactions_created_status 
ON transactions(created_at, status);
-- Time: 15ms (использует индекс, сканирует 500 строк)

Причина 3: Рост объёма данных

Олучше:
  - 1000 строк: запрос 10ms
  - 10000 строк: запрос 50ms (линейный рост)

Плохо:
  - 1000 строк: запрос 10ms
  - 10000 строк: запрос 500ms (квадратичный рост)
  → Нет индекса или неэффективный алгоритм

Причина 4: Memory leak в кэше

// ПЛОХО: кэш растёт бесконечно
@Service
public class CachingService {
    private Map<Long, User> userCache = new HashMap<>();
    
    public User getUser(Long id) {
        if (!userCache.containsKey(id)) {
            User user = repository.findById(id).orElse(null);
            userCache.put(id, user); // Растёт бесконечно!
        }
        return userCache.get(id);
    }
}

// ХОРОШО: с ограничением размера
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("users");
    }
}

// Или с Spring Cache
@Service
public class UserService {
    @Cacheable(value = "users", unless = "#result == null")
    public User getUser(Long id) {
        return repository.findById(id).orElse(null);
    }
}

Причина 5: Неправильная конфигурация БД

# application.yml
spring:
  jpa:
    properties:
      hibernate:
        jdbc.batch_size: 20 # Батчинг
        order_inserts: true
        order_updates: true
        generate_statistics: true
  datasource:
    hikari:
      maximum-pool-size: 10 # Пул соединений
      connection-timeout: 20000

Шаг 4: Инструменты для диагностики

1. JVM Profiler

# JProfiler, YourKit
# Показывает: какой метод занимает сколько времени

# Или встроенный jstat
jstat -gc <pid> 1000 # Смотрим GC каждую секунду

# Если много GC паузы → есть утечка памяти или неэффективное выделение

2. DataSource Profiler

<!-- pom.xml -->
<dependency>
    <groupId>org.bgee.log4jdbc-log4j2</groupId>
    <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
    <version>1.16</version>
</dependency>

<!-- application.properties -->
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:postgresql://localhost:5432/mydb

<!-- Теперь видим все SQL с временем выполнения -->

3. APM решения

// New Relic, DataDog, Elastic APM
// Показывают: какие запросы медленные, где узкие места

// Например, Micrometer
@Configuration
public class MetricsConfig {
    @Bean
    public MeterFilter apiHttpRequestsTimer() {
        return new MeterFilter() {
            @Override
            public void configure(Meter.Id id, MeterConfig config) {
                if (id.getName().startsWith("http.server.requests")) {
                    config.tag("endpoint", id.getTag("uri"));
                }
            }
        };
    }
}

Методический процесс оптимизации

Шаг 1: Измерь

Сколько времени занимает запрос?
  Можно использовать: Stopwatch, System.currentTimeMillis(), @Timed

Шаг 2: Профилируй

Где тратится время?
  - SQL выполнение?
  - Маршаллинг/сериализация?
  - Сетевая задержка?
  - Обработка в Java?

Шаг 3: Оптимизируй

Вариант 1: Оптимизировать SQL
  - Добавить индекс
  - Переписать запрос
  - Кэшировать результаты

Вариант 2: Оптимизировать Java код
  - Избежать N+1
  - Использовать connection pooling
  - Параллелизовать

Вариант 3: Архитектурные изменения
  - Разделить монолит
  - Добавить кэширующий слой
  - Использовать message queue

Шаг 4: Проверь

Повторные измерения:
  - До оптимизации: 1000ms
  - После оптимизации: 100ms
  - Улучшение: 10x

Пример полной диагностики

@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;
    
    @GetMapping("/orders")
    public ResponseEntity<List<Order>> getAllOrders() {
        Stopwatch sw = Stopwatch.createStarted();
        
        List<Order> orders = orderService.getAllOrders();
        
        long duration = sw.stop().elapsed(TimeUnit.MILLISECONDS);
        if (duration > 100) {
            logger.warn("getAllOrders took {}ms", duration);
        }
        
        return ResponseEntity.ok(orders);
    }
}

@Service
public class OrderService {
    @Autowired
    private OrderRepository repository;
    
    // С EXPLAIN ANALYZE видим, что SQL медленный
    // Проблема: нет индекса по status
    public List<Order> getAllOrders() {
        return repository.findByStatus("PENDING");
    }
}

// Решение:
public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query(value = "SELECT * FROM orders WHERE status = ?1 ORDER BY created_at DESC",
           nativeQuery = true)
    List<Order> findByStatus(String status);
}

// И в миграции:
CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC);

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

  • Включил SQL логирование
  • Запустил EXPLAIN ANALYZE
  • Проверил индексы на столбцах из WHERE
  • Проверил N+1 запросы
  • Проверил размер результата (не загружаю лишние поля)
  • Добавил кэширование если нужно
  • Профилировал JVM (нет ли memory leak)
  • Проверил пул соединений БД
  • Пересчитал метрики после оптимизации

Вывод

Когда запрос замедляется — это НЕ магия. Всегда есть конкретная причина:

  1. Добавилось данных → нужен индекс
  2. N+1 запросы → нужна JOIN FETCH
  3. Memory leak → нужен профайлер
  4. Неправильный SQL → нужен EXPLAIN
  5. Сетевая задержка → нужна асинхронность

Методический подход (измерь → профилируй → оптимизируй → проверь) всегда даст результат.