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

Каталог из нескольких тысяч товаров может тормозить сильнее, чем база с миллионами записей: достаточно добавить поиск по подстроке, сортировку по цене и фильтр по категории. Причина обычно не в одном «плохом» запросе, а в сочетании условий. Ниже — практический разбор для веб-приложения на PostgreSQL 16: как выбирать способ поиска, проектировать фильтры и проверять, что запросы останутся быстрыми.
Сначала разделите поиск и фильтрацию
Пользователь воспринимает поиск и фильтры как одну форму. Для базы это разные операции. Полнотекстовый поиск находит документы по словам и их формам; фильтр ограничивает результат точными значениями — категорией, статусом, диапазоном цены. Сортировка задаёт порядок уже найденных строк.
Когда все условия собирают в один универсальный запрос — например, с большим числом необязательных параметров и проверками вида «параметр пустой или поле равно параметру», — планировщику сложнее подобрать эффективный план. Особенно если разные сочетания параметров возвращают совершенно разное число строк.
Проектируйте не «один запрос на все случаи», а несколько понятных сценариев и проверяйте каждый на данных, похожих на боевые.
До выбора индекса зафиксируйте типичные сценарии: поиск по названию, фильтр категории, диапазон цены, комбинация нескольких условий, сортировка и переход на следующую страницу. Для каждого важны не только параметры, но и ожидаемый размер результата.
Выберите способ поиска по тому, что вводит пользователь
В PostgreSQL 16 есть несколько разных способов искать текст. Они не взаимозаменяемы: полнотекстовый поиск подходит для слов, но не обещает найти произвольную подстроку; поиск по подстроке удобен для фрагментов артикула, но может потребовать другого индекса.
| Сценарий | Подход | Цена и ограничение |
|---|---|---|
| Запрос из слов: «красная куртка» | Полнотекстовый поиск PostgreSQL | Нужны настройка языковой конфигурации и индекс; поиск ориентирован на слова, а не на любые фрагменты внутри слова. |
| Фрагмент названия или артикула | Сравнение с подстрокой | Условие с шаблоном может не использовать обычный B-tree индекс. Для масштабирования проверьте специализированный индекс и его настройки. |
| Точное значение: артикул, код, статус | Равенство по нормализованному полю | Предсказуемо и просто индексировать, но пользователь должен знать точное значение. |
Для полнотекстового поиска в PostgreSQL 16 можно создать GIN-индекс на выражение, а в запросе использовать то же выражение. Пример предполагает таблицу products с текстовым полем name и конфигурацию поиска russian:
CREATE INDEX products_name_search_idx
ON products
USING GIN (to_tsvector('russian', name));
SELECT id, name
FROM products
WHERE to_tsvector('russian', name)
@@ plainto_tsquery('russian', 'красная куртка');
Это пример именно полнотекстового поиска по словам, а не поиска подстроки. Если нужно находить фрагмент артикула или часть слова, сначала измерьте запрос с реальным шаблоном и изучите раздел документации PostgreSQL о расширении pg_trgm: там описаны триграммные индексы и условия их применения. Не добавляйте индекс «на всякий случай» — каждый индекс занимает место и увеличивает цену изменений строк.
Фильтры: индексы под частые сочетания
Индекс на каждое поле не гарантирует быстрый комбинированный запрос. Если пользователи почти всегда выбирают категорию и статус, составной индекс может оказаться полезнее нескольких отдельных. Но порядок полей имеет значение: он должен соответствовать реальным условиям и сортировке. Универсального порядка для всех каталогов нет.
Допустим, типичный запрос отбирает товары одной категории, оставленные в наличии, и сортирует по цене. Возможную структуру индекса можно проверить на копии схемы:
CREATE INDEX products_category_stock_price_idx
ON products (category_id, in_stock, price);
Это не рекомендация создавать индекс автоматически. Если фильтр по наличию почти ничего не исключает или категория не задаётся в большинстве запросов, такой порядок может не помочь. Сверяйте план выполнения и долю строк, которую база читает ради результата.
Не превращайте фильтры в ловушку для плана
Условия с необязательными параметрами удобны для интерфейса, но разные сочетания фильтров способны требовать разных планов. Если один сценарий быстрый, а другой медленный, разбейте запросы по сценариям или проверьте, не мешает ли универсальная конструкция использовать подходящий индекс. Решение подтверждайте измерением, а не только видом SQL.
Селективность — доля строк, которую условие оставляет после фильтрации. Категория с десятком товаров селективна; признак, установленный у 95% каталога, обычно отбирает мало. Поэтому индекс на редкий статус и индекс на массовый флаг могут дать разный эффект.
Сортировка и пагинация — часть запроса
Фильтры могут работать быстро, а выдача всё равно будет медленной из-за сортировки. Поддерживайте ограниченный список сортировок, которые действительно нужны интерфейсу, и проверяйте их вместе с фильтрами. Сортировка по полю без подходящего индекса может заставить базу упорядочивать большой промежуточный набор.
Пагинация через смещение проста: запрос пропускает заданное число строк и возвращает следующую страницу. Но чем дальше страница, тем больше строк может понадобиться пропустить. Для длинной ленты рассмотрите пагинацию по ключу: передавайте значение последней строки и выбирайте следующую порцию по упорядоченному ключу. При равных ценах нужен дополнительный уникальный критерий порядка, например идентификатор, иначе элементы могут перескакивать между страницами.
Не обещайте одинаковую выдачу при параллельных изменениях каталога без отдельной стратегии согласованности. Если товары добавляются или меняют цену между запросами, пользователь может увидеть повтор или пропуск — особенно при смещении.
Как проверить решение до релиза
- Соберите реальные сценарии. Зафиксируйте сочетания поиска, фильтров и сортировок, которые доступны в интерфейсе. Проверьте, что среди них нет неограниченного набора сортировок по любому полю.
- Подготовьте реалистичные данные. Важны распределения: число товаров в категориях, частота статусов, длина названий и доля совпадений. Проверка на десятке строк мало что говорит о поведении на рабочем объёме.
- Посмотрите план выполнения. В PostgreSQL используйте
EXPLAIN; для оценки фактического выполнения и времени в тестовой среде —EXPLAIN ANALYZE. Последний действительно запускает запрос, поэтому осторожно применяйте его к тяжёлым операциям и производственной базе. - Сравните несколько вариантов. Проверьте запрос до и после индекса на одинаковом наборе данных. Запишите время, число прочитанных строк и план, а не только субъективное ощущение «стало быстрее».
- Проверьте цену записи. Индекс ускоряет некоторые чтения, но занимает место и добавляет работу при вставках и изменениях. Измерьте не только выдачу, но и операции обновления каталога.
В плане особенно полезно сравнить оценку числа строк с фактическим числом. Большой разрыв может указывать на неудачные оценки статистики или необычное распределение данных. Сам по себе тип узла плана не доказывает проблему: важны объём чтения, повторяемость запроса и замер на характерных данных.
Что ломается в типичных реализациях
- Обычный индекс ждут от поиска по фрагменту. Индекс на текстовом столбце не означает, что условие с подстановочным знаком в начале строки будет быстрым. Проверяйте конкретное условие и подходящую стратегию поиска.
- Создают индекс под редкий запрос. Он занимает место, замедляет изменения данных и может почти не встречаться в работе. Начинайте с наблюдаемых сценариев.
- Сортируют по пользовательскому выражению. Если интерфейс позволяет выбрать десятки комбинаций сортировки, число нужных индексов быстро растёт. Ограничьте выбор теми порядками, которые нужны людям.
- Считают общее число результатов на каждом вводе. Точный подсчёт может оказаться дорогим отдельно от получения страницы. Решите, действительно ли интерфейсу нужно точное число или достаточно показывать следующую страницу, пока она существует.
Инвалидация кеша результатов и поиск с опечатками — отдельные задачи; здесь речь только о запросах, индексах и выдаче.
Что делать дальше
Выберите три самых частых сценария поиска и фильтрации, воспроизведите их на копии данных и сохраните планы выполнения. После релиза следите за временем запросов и количеством прочитанных строк именно для этих сценариев. Если поведение изменилось после роста каталога, сравните новые планы со старыми и только потом добавляйте индексы.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: PostgreSQL 16: полнотекстовый поиск, PostgreSQL 16: EXPLAIN
Частые вопросы
Нужен ли полнотекстовый поиск для каталога товаров?
Почему поиск по подстроке медленный, хотя на поле есть индекс?
Стоит ли создавать индекс на каждый фильтр?
Что выбрать для пагинации: OFFSET или курсор?
Как понять, что индекс действительно ускорил запрос?
Нужно ли считать точное количество результатов для каждой страницы?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.