Code Review: Метод find в сервисе интернет-магазина

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() и сразу получать товары одним запросом.