24. Оптимизировать поиск товаров по фильтру
Условие задачи:
Пользователь интернет-магазина задаёт несколько фильтров для поиска товаров. Метод find(Filter filter) сначала выполняет запрос на подсчёт подходящих товаров и только при ненулевом результате загружает сами товары.
Метод вызывается около 200 раз в секунду, при этом примерно в 50% случаев подходящих товаров нет.
Нужно провести code review метода и предложить более эффективную реализацию с меньшей нагрузкой на БД.
Код:
class Service {
//200 RPS
//50% count = 0
public List<Product> find(Filter filter) {
int count = repo.count(filter);
if(count > 0) {
return repo.find(filter);
}
return new ArrayList<>();
}
}
Спойлеры к решению
Подсказки
count(), если после него всё равно требуется получить товары.💡 Посчитай реальное количество запросов к БД при 200 RPS и 50% пустых результатов.
💡 Сам запрос поиска может вернуть пустой список.
💡 Для большого количества результатов стоит подумать о пагинации.
Решение
Проблемы:
выполняется лишний запрос
count();при наличии товаров выполняются два запроса:
COUNTиSELECT;при 200 RPS получается около 300 запросов к БД в секунду вместо 200;
между
count()иfind()данные могут измениться;создание
new ArrayList<>()для пустого результата не требуется;при большом количестве найденных товаров отсутствует пагинация;
производительность поиска зависит от SQL и индексов по полям фильтрации.
Как лучше исправить
Если нужен список товаров, предварительный подсчёт вообще не требуется:
class Service {
public List<Product> find(Filter filter) {
return repo.find(filter);
}
}
Репозиторий должен вернуть пустой список, если товаров нет. Дополнительная проверка isEmpty() тоже не нужна.
При текущем подходе нагрузка выглядит так:
200 COUNT-запросов
+ 100 SELECT-запросов
= 300 запросов/сек
После удаления count() останется один запрос на каждый вызов метода — около 200 запросов в секунду.
Если результатов может быть много, лучше добавить пагинацию:
public Slice<Product> find(
Filter filter,
Pageable pageable
) {
return repo.find(filter, pageable);
}
Если общее количество найденных товаров не требуется, Slice обычно предпочтительнее Page, поскольку для Page требуется знать общее число результатов и часто выполняется дополнительный COUNT(*).
Кэширование можно рассматривать только если одинаковые фильтры часто повторяются и допустима некоторая устарелость данных. Добавлять кэш только потому, что половина запросов возвращает пустой результат, недостаточно.
Для сложных фильтров также нужно проверить фактический SQL и подобрать индексы под наиболее часто используемые условия.
Главное исправление — убрать предварительный count() и сразу получать товары одним запросом.