← Назад к вопросам
Как поступишь, если тормозит запрос к БД
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;
}
}
}
Пошаговый чеклист при проблеме
- ✓ Измерить время — Сколько ровно занимает?
- ✓ EXPLAIN ANALYZE — Что БД фактически выполняет?
- ✓ Проверить индексы — Они используются?
- ✓ Проверить N+1 — Сколько запросов выполняется?
- ✓ Проверить JOIN'ы — Дублируются ли данные?
- ✓ Попробовать пагинацию — Нужно ли загружать все?
- ✓ Добавить кэширование — Часто ли повторяется запрос?
- ✓ Проверить ресурсы — Хватает ли памяти/соединений?
- ✓ Профилировать — Где именно тормозит код?
- ✓ Масштабировать — Нужна ли репликация/шардинг?
Инструменты анализа
- EXPLAIN ANALYZE — встроенный анализатор запросов
- Hibernate Statistics —
hibernate.generate_statistics=true - Query Profiler — p6spy, datasource-proxy
- APM — New Relic, DataDog, Elastic
- JMeter — нагрузочное тестирование
Помни: перед оптимизацией — измери, перед масштабированием — оптимизируй!