АрхитектураПроектирование системИнженер по проектированию backend-систем

Клиент требует возвращать точное общее число совпадений в каждом ответе API поиска. Какой механизм делает э...

Клиент требует возвращать точное общее число совпадений в каждом ответе API поиска. Какой механизм делает это требование потенциально дорогим?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Для точного total_count сервер должен определить количество всех записей, соответствующих фильтрам, а не только загрузить страницу. Это может потребовать отдельного агрегатного прохода по большому набору данных, поэтому стоимость и задержка запроса начинают зависеть от размера всего результата, а не от размера страницы.

Исторический контекст

Постраничная выдача изначально решала задачу ограничения объёма данных, передаваемых за один запрос. Клиенту обычно нужны только ближайшие элементы, поэтому сервер мог обработать небольшую страницу и не просматривать весь набор совпадений.

Требование точного общего количества появилось как удобство для интерфейсов: построения номера последней страницы, отображения прогресса и оценки масштаба результата. Оно добавляет к выдаче информацию, которая не всегда нужна для навигации.

Постановка проблемы

Запрос страницы с лимитом, например, может остановиться после нахождения нужного числа записей. Для точного количества серверу нужно дополнительно посчитать все совпадения по тем же фильтрам, часто с учётом соединений, сортировки или проверки доступности данных.

На большой таблице это увеличивает нагрузку на CPU, диск и буферный кэш базы данных. При высокой конкуренции точный подсчёт может замедлить не только поиск, но и другие операции, использующие те же ресурсы.

Особенно опасна ситуация, когда интерфейс вызывает поиск при каждом изменении фильтра, а сервер каждый раз выполняет дорогой точный подсчёт. Масштабирование только количеством экземпляров приложения не всегда помогает, если узким местом остаётся общее хранилище.

Подробное решение

Сервер обычно выполняет два логических действия: получает ограниченную страницу записей и вычисляет количество совпадений. Даже если база может использовать индекс для подсчёта, ей всё равно необходимо проверить множество подходящих записей; индекс снижает стоимость, но не делает операцию бесплатной.

Нужно проверить план выполнения, селективность фильтров, наличие подходящих индексов и фактическую задержку на типичных и худших запросах. Важно измерять не только среднее время, но и хвостовые значения задержки при одновременной нагрузке.

Если точность не обязательна, можно возвращать признак наличия следующей страницы вместо полного количества. Для этого достаточно запросить на один элемент больше лимита: если дополнительная запись найдена, следующая страница существует. Такой подход ограничивает работу размером страницы.

Другие варианты зависят от требований:

  • возвращать приблизительное количество с явным указанием неточности;
  • вычислять количество асинхронно или кэшировать его для повторяющихся запросов;
  • ограничивать диапазон, в котором выполняется точный подсчёт;
  • поддерживать отдельный индекс или поисковое хранилище, если поиск является самостоятельной подсистемой.

Кэширование эффективно только при повторяемых запросах и приемлемой давности результата. Отдельное поисковое хранилище ускоряет поиск, но добавляет задержку обновления, стоимость синхронизации и необходимость контролировать расхождение с источником данных.

Главный компромисс — удобство точной навигации против предсказуемой стоимости запроса. Если бизнесу нужны только элементы и возможность перейти дальше, has_next обычно безопаснее, чем точный total_count.

Ситуация из практики

В каталоге товаров интерфейс запрашивал 20 товаров на страницу и точное число совпадений для каждого набора фильтров. На популярных фильтрах выдача была быстрой, но комбинации по малоиндексируемым атрибутам заставляли хранилище просматривать большой объём данных.

Рассматривались три варианта. Добавление индексов могло ускорить часть запросов, но увеличивало стоимость записи и не покрывало произвольные комбинации фильтров. Кэширование количества уменьшало повторную нагрузку, но не решало проблему новых запросов и устаревших значений. Перенос поиска в отдельную систему давал более подходящую модель выполнения, но требовал синхронизации каталога и принятия временной несогласованности.

Выбрали выдачу страницы с признаком наличия следующей страницы, а точное количество оставили только для отдельных сценариев с ограниченным диапазоном результатов. Это устранило обязательный полный подсчёт из горячего пути и сделало стоимость обычного запроса зависимой главным образом от размера страницы. Точные значения для редких сценариев выполнялись отдельно и контролировались по задержке и нагрузке.

Что кандидаты часто упускают

  1. Всегда ли отдельный запрос подсчёта дешевле, чем получение страницы?

Нет. Получение страницы может завершиться после чтения небольшого числа подходящих записей, тогда как подсчёт должен обработать весь набор совпадений. При низкой селективности фильтра или большом результате отдельный подсчёт становится основной частью работы. Кроме того, два запроса увеличивают конкуренцию за соединения и ресурсы хранилища.

  1. Почему индекс не гарантирует дешёвый точный подсчёт?

Индекс помогает быстро находить кандидатов, но для точного результата системе может понадобиться проверить большое количество индексных записей и видимость соответствующих строк. При сложных фильтрах, соединениях или дополнительных условиях одного индекса недостаточно. Индекс также имеет цену: он занимает место и замедляет изменения данных.

  1. Можно ли безопасно кэшировать точное количество?

Только если допустима определённая давность результата или есть механизм корректного обновления. Между вычислением значения и чтением клиентом данные могут измениться, поэтому кэш не гарантирует точность в реальном времени. Нужно определить допустимый срок устаревания, ключ кэша, правила инвалидирования и защиту от одновременного заполнения кэша одинаковыми запросами.