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

Как поступишь, если тормозит запрос к БД

2.0 Middle🔥 211 комментариев
#Базы данных и SQL

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

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

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

# Как поступишь, если тормозит запрос к БД

Методология диагностики и оптимизации

Когда запрос к БД работает медленно, нужно действовать систематически. Вот пошаговый процесс профессионального разработчика.

Шаг 1: Локализация проблемы

1.1 Определить где именно происходит задержка

@Service
@Slf4j
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    public User getUserWithTiming(Long id) {
        // Измерить время запроса
        long startTime = System.currentTimeMillis();
        
        try {
            User user = userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException(id));
            
            long duration = System.currentTimeMillis() - startTime;
            log.info("Database query took {} ms", duration);
            
            return user;
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;
            log.error("Query failed after {} ms: {}", duration, e.getMessage());
            throw e;
        }
    }
}

// Или использовать @Timed аннотацию
@Service
public class TimedUserService {
    
    @Timed(value = "user.fetch", description = "Time taken to fetch user")
    public User getUser(Long id) {
        // Код
        return null;
    }
}

1.2 Анализ логов БД

-- PostgreSQL: включить логирование медленных запросов
SET log_min_duration_statement = 1000;  -- Логировать запросы > 1 сек

-- MySQL
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- Запросы > 1 сек

-- Проверить статистику
SHOW SLOW LOGS;

Шаг 2: Анализ запроса — EXPLAIN PLAN

-- PostgreSQL/MySQL
EXPLAIN ANALYZE
SELECT * FROM users WHERE id = 1;

-- Результат покажет:
-- - Seq Scan vs Index Scan
-- - Количество строк
-- - Стоимость
-- - Фильтры (Filter clauses)

Интерпретация результатов

-- ПЛОХО: Sequential Scan (полное сканирование таблицы)
Seq Scan on users  (cost=0.00..35.50 rows=1 width=100)
  Filter: (id = 1)

-- ХОРОШО: Index Scan (использование индекса)
Index Scan using users_pkey on users  (cost=0.29..8.30 rows=1 width=100)
  Index Cond: (id = 1)

Шаг 3: Основные причины и решения

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

// ПЛОХО: Без индекса
@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;
    
    private String email;  // Нет индекса!
    private String name;
}

// ХОРОШО: С индексом
@Entity
@Table(name = "users", indexes = {
    @Index(columnList = "email", name = "idx_user_email")
})
public class User {
    @Id
    private Long id;
    
    @Column(unique = true)
    @Index(name = "idx_user_email")  // Индекс
    private String email;
    
    private String name;
}
-- SQL миграция
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_created_at ON orders(created_at DESC);

Причина 2: N+1 проблема (лишние запросы)

// ПЛОХО: N+1 запросов
@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;
    
    @OneToMany(fetch = FetchType.LAZY)  // Lazy loading!
    private List<Order> orders;  // Каждый order = отдельный запрос
}

// Код
public List<User> getUsers() {
    List<User> users = userRepository.findAll();  // 1 запрос
    
    for (User user : users) {
        System.out.println(user.getOrders());  // N запросов!
    }
    
    return users;  // Всего: 1 + N запросов
}

// ХОРОШО: Eager loading или JOIN FETCH
public List<User> getUsersOptimized() {
    // Способ 1: FETCH с одним запросом
    return userRepository.findAllWithOrders();
}

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

Причина 3: Неправильные JOIN'ы

// ПЛОХО: Картезиев продукт (много дублей)
@Query("""n    SELECT u FROM User u 
        LEFT JOIN u.orders o
        LEFT JOIN u.profiles p
        """)
List<User> findUsersWithJoins();  // Дублирование данных

// ХОРОШО: Используй DISTINCT и DTO
@Query("""n    SELECT DISTINCT u FROM User u 
        LEFT JOIN FETCH u.orders
        """)
List<User> findUsersWithOrders();

// Или используй projection
@Query("""n    SELECT new com.example.UserDTO(u.id, u.name, COUNT(o))
        FROM User u
        LEFT JOIN u.orders o
        GROUP BY u.id, u.name
        """)
List<UserDTO> findUsersWithOrderCount();

Причина 4: Полное сканирование таблицы (таблица слишком велика)

// ПЛОХО: Загружаем все в памяь
public List<User> getAllUsers() {
    return userRepository.findAll();  // 1 млн записей в памяти!
}

// ХОРОШО: Пагинация
public Page<User> getUsersPaginated(int page, int size) {
    return userRepository.findAll(
        PageRequest.of(page, size, Sort.by("id").ascending())
    );
}

// Или streaming
public void processUsersInBatches() {
    int pageSize = 1000;
    int page = 0;
    
    Page<User> users;
    do {
        users = userRepository.findAll(
            PageRequest.of(page++, pageSize)
        );
        users.getContent().forEach(this::processUser);
    } while (users.hasNext());
}

Причина 5: Неправильно написанный запрос

// ПЛОХО: Функция на WHERE условии
@Query("""n    SELECT u FROM User u
        WHERE LOWER(u.name) = LOWER(:name)
        """)  // LOWER() не может использовать индекс
List<User> findByNameIgnoreCase(String name);

// ХОРОШО: Правильная нормализация
@Query("""n    SELECT u FROM User u
        WHERE u.nameNormalized = :nameLower
        """)
List<User> findByNormalizedName(String nameLower);

// Или используй встроенные возможности
public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByNameIgnoreCase(String name);  // Spring генерирует LOWER()
}

Причина 6: Недостаточные ресурсы БД

// Проверить соединения
@Service
public class ConnectionPoolMonitor {
    
    @Autowired
    private DataSource dataSource;
    
    public void checkPoolHealth() {
        HikariDataSource hikariDs = (HikariDataSource) dataSource;
        
        log.info("Active connections: {}", 
            hikariDs.getHikariPoolMXBean().getActiveConnections());
        log.info("Idle connections: {}", 
            hikariDs.getHikariPoolMXBean().getIdleConnections());
        log.info("Pending threads: {}", 
            hikariDs.getHikariPoolMXBean().getThreadsAwaitingConnection());
    }
}

// Увеличить пул соединений
@Bean
public DataSource dataSource() {
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl("jdbc:postgresql://localhost/mydb");
    config.setMaximumPoolSize(30);  // Было 10
    config.setMinimumIdle(10);       // Было 5
    return new HikariDataSource(config);
}

Шаг 4: Оптимизация — Стратегии кэширования

4.1 Redis кэш

@Service
@CacheConfig(cacheNames = "users")
public class CachedUserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Cacheable(key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    @CachePut(key = "#user.id")
    public User updateUser(User user) {
        return userRepository.save(user);
    }
}

// spring-boot-starter-data-redis
@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        return RedisCacheManager.create(factory);
    }
}

4.2 Query результаты кэширования

public interface UserRepository extends JpaRepository<User, Long> {
    
    @Cacheable(value = "users", key = "#id")
    @Query("SELECT u FROM User u WHERE u.id = :id")
    Optional<User> findById(@Param("id") Long id);
    
    @Cacheable(value = "usersByEmail", key = "#email")
    @Query("SELECT u FROM User u WHERE u.email = :email")
    Optional<User> findByEmail(@Param("email") String email);
}

Шаг 5: Мониторинг и метрики

@Configuration
public class QueryMetricsConfig {
    
    @Bean
    public MeterRegistry meterRegistry() {
        return new PrometheusMeterRegistry(new PrometheusConfig());
    }
}

@Aspect
@Component
@Slf4j
public class QueryPerformanceMonitor {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    @Around("@annotation(com.example.annotations.MonitorQuery)")
    public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().getName();
        long startTime = System.nanoTime();
        
        try {
            Object result = pjp.proceed();
            long duration = System.nanoTime() - startTime;
            
            meterRegistry.timer("db.query.duration", 
                "method", methodName).record(duration, TimeUnit.NANOSECONDS);
            
            if (duration > 5_000_000_000L) {  // > 5 сек
                log.warn("Slow query: {} took {} ms", 
                    methodName, duration / 1_000_000);
            }
            
            return result;
        } catch (Exception e) {
            meterRegistry.counter("db.query.errors", 
                "method", methodName).increment();
            throw e;
        }
    }
}

Пошаговый чеклист при проблеме

  1. Измерить время — Сколько ровно занимает?
  2. EXPLAIN ANALYZE — Что БД фактически выполняет?
  3. Проверить индексы — Они используются?
  4. Проверить N+1 — Сколько запросов выполняется?
  5. Проверить JOIN'ы — Дублируются ли данные?
  6. Попробовать пагинацию — Нужно ли загружать все?
  7. Добавить кэширование — Часто ли повторяется запрос?
  8. Проверить ресурсы — Хватает ли памяти/соединений?
  9. Профилировать — Где именно тормозит код?
  10. Масштабировать — Нужна ли репликация/шардинг?

Инструменты анализа

  • EXPLAIN ANALYZE — встроенный анализатор запросов
  • Hibernate Statisticshibernate.generate_statistics=true
  • Query Profiler — p6spy, datasource-proxy
  • APM — New Relic, DataDog, Elastic
  • JMeter — нагрузочное тестирование

Помни: перед оптимизацией — измери, перед масштабированием — оптимизируй!

Как поступишь, если тормозит запрос к БД | PrepBro