Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как решить проблему долгого запроса в БД
Медленные запросы — это одна из главных проблем в масштабируемых приложениях. Стратегия решения зависит от диагностики проблемы. Начнём с систематического подхода: измерение, анализ и оптимизация.
Шаг 1: Диагностика — найти виновника
Включи логирование медленных запросов
# application.yml
spring:
jpa:
properties:
hibernate:
format_sql: true
use_sql_comments: true
generate_statistics: true
sql:
init:
data-locations: classpath:sql/data.sql
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
org.springframework.jdbc.core: DEBUG
Профилировать запросы в PostgreSQL
-- PostgreSQL: EXPLAIN ANALYZE
EXPLAIN ANALYZE
SELECT u.id, u.name, o.id, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = ACTIVE;
-- Результат покажет:
-- Seq Scan / Index Scan
-- Actual time=
-- Rows=
-- Total cost=
Использование Micrometer для метрик
@Configuration
public class JpaMetricsConfig {
@Bean
public MeterBinder hibernateMetrics() {
return (registry) -> {
// Spring автоматически собирает метрики
};
}
}
// Проверь через Actuator
// GET http://localhost:8080/actuator/metrics/hibernate.sessions.open
Шаг 2: Типичные проблемы и их решения
Проблема 1: N+1 SELECT запросы
Это когда один запрос к заказам вызывает N дополнительных запросов к пользователям.
// ❌ НЕПРАВИЛЬНО: N+1 селект
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
private User user; // Lazy loading
}
List<Order> orders = repository.findAll();
for (Order o : orders) {
System.out.println(o.getUser().getName()); // N дополнительных SELECT
}
// ✅ РЕШЕНИЕ 1: JOIN FETCH
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUser();
// ✅ РЕШЕНИЕ 2: Аннотация @EntityGraph
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = {"user"})
List<Order> findAll();
@EntityGraph(attributePaths = {"user", "items"})
Optional<Order> findById(Long id);
}
// ✅ РЕШЕНИЕ 3: Проекция DTO
@Query("SELECT new com.example.OrderDTO(o.id, u.name, o.amount) " +
"FROM Order o JOIN o.user u")
List<OrderDTO> findAllWithUserName();
Проблема 2: Отсутствие индексов
-- Проверить индексы
SELECT * FROM pg_indexes WHERE tablename = orders;
-- Добавить индекс
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_created_date ON orders(created_date DESC);
-- Составной индекс для частых условий
CREATE INDEX idx_orders_status_user ON orders(status, user_id);
В Spring Data:
@Entity
@Table(name = "orders", indexes = {
@Index(name = "idx_user_id", columnList = "user_id"),
@Index(name = "idx_status", columnList = "status"),
@Index(name = "idx_created_date", columnList = "created_date DESC")
})
public class Order {
@Id
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "status")
private OrderStatus status;
}
Исправляющая миграция Goose:
-- migrations/0010_add_order_indexes.sql
-- +goose Up
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status_created ON orders(status, created_date DESC);
-- +goose Down
DROP INDEX IF EXISTS idx_orders_user_id;
DROP INDEX IF EXISTS idx_orders_status_created;
Проблема 3: Неэффективные WHERE условия
-- ❌ ПЛОХО: функция на поле индекса
WHERE EXTRACT(YEAR FROM created_date) = 2026;
WHERE LOWER(name) = john; -- Не использует индекс
-- ✅ ХОРОШО: сравнение с диапазоном
WHERE created_date >= 2026-01-01 AND created_date < 2027-01-01;
WHERE name = John; -- Использует индекс
-- ❌ ПЛОХО: OR вместо IN
WHERE status = ACTIVE OR status = PENDING;
-- ✅ ХОРОШО: IN
WHERE status IN (ACTIVE, PENDING);
Проблема 4: Большие наборы данных без пагинации
// ❌ НЕПРАВИЛЬНО
List<Order> all = repository.findAll(); // Все 1 млн записей в памяти!
// ✅ ПРАВИЛЬНО
Page<Order> page = repository.findAll(
PageRequest.of(0, 100, Sort.by("createdDate").descending())
);
while (page.hasNext()) {
processOrders(page.getContent());
page = repository.findAll(
PageRequest.of(page.getNumber() + 1, 100)
);
}
Шаг 3: Оптимизация на уровне Java
Кэширование запросов
@Service
public class OrderService {
@Cacheable(value = "orders", key = "#userId")
public List<Order> getUserOrders(Long userId) {
return repository.findByUserId(userId);
}
@CacheEvict(value = "orders", key = "#order.userId")
public void saveOrder(Order order) {
repository.save(order);
}
}
Использование Projection для уменьшения данных
public interface OrderSummaryDTO {
Long getId();
BigDecimal getAmount();
}
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// Загружаем только нужные поля
List<OrderSummaryDTO> findByUserId(Long userId);
}
Пакетная обработка (batch)
@Service
public class BatchProcessingService {
@Autowired
private OrderRepository repository;
@Autowired
private SessionFactory sessionFactory;
public void processLargeDataset(List<Order> orders) {
int batchSize = 100;
for (int i = 0; i < orders.size(); i++) {
Order order = orders.get(i);
// Обработка
order.setProcessed(true);
repository.save(order);
// Каждые batchSize элементов очищаем сессию
if (i % batchSize == 0) {
sessionFactory.getCurrentSession().flush();
sessionFactory.getCurrentSession().clear();
}
}
}
}
Шаг 4: Оптимизация на уровне БД
Статистика
-- PostgreSQL
ANALYZE orders; -- Обновить статистику
VACUUM ANALYZE orders; -- Очистка и анализ
-- Проверить план выполнения
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 123;
Раздел таблицы (partitioning)
-- Разделить большую таблицу по дате
CREATE TABLE orders_2026 PARTITION OF orders
FOR VALUES FROM (2026-01-01) TO (2027-01-01);
Денормализация (если нужна скорость больше, чем консистентность)
-- Денормализованная таблица с часто нужными данными
CREATE TABLE user_order_summary AS
SELECT u.id, u.name, COUNT(*) 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;
-- Обновлять через scheduled job
Шаг 5: Асинхронная обработка
Если запрос дорогой и не срочный:
@Service
public class AsyncReportService {
@Async
public CompletableFuture<Report> generateReport(Long userId) {
// Длительный запрос в отдельном потоке
List<Order> orders = repository.findByUserId(userId);
Report report = computeReport(orders);
return CompletableFuture.completedFuture(report);
}
}
// Использование
@RestController
public class ReportController {
@GetMapping("/reports/{userId}")
public DeferredResult<Report> getReport(@PathVariable Long userId) {
DeferredResult<Report> result = new DeferredResult<>();
reportService.generateReport(userId).thenAccept(report -> {
result.setResult(report);
}).exceptionally(ex -> {
result.setErrorResult(ex);
return null;
});
return result;
}
}
Чеклист диагностики и оптимизации
- Измерить — EXPLAIN ANALYZE, логи, метрики
- Найти — N+1, отсутствие индексов, неправильные WHERE
- Оптимизировать по приоритету:
- Добавить индексы (обычно -50% времени)
- JOIN FETCH вместо N+1 (-40%)
- Пагинация и проекции (-30%)
- Кэширование (-60% на повторяющихся запросах)
- Денормализация (последний вариант)
- Тестировать — проверить улучшения через метрики
- Мониторить — постоянно отслеживать slow query logs
Правило 80/20: 20% запросов обычно занимают 80% времени. Сфокусируй на них.