15 нояб. 2025 г.·7 мин чтения

Индексы базы данных для страниц поиска после реального трафика

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

Индексы базы данных для страниц поиска после реального трафика

Почему ранние индексы перестают соответствовать реальному поиску

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

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

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

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

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

В этом и заключается проблема. Ранняя настройка отражает продуктовые предположения. Пост‑запусковая настройка должна отражать реальные паттерны фильтров в продакшн, фактическое использование сортировок и то, где база затратит время сейчас. Иначе команды продолжают шлифовать не тот путь запроса, пока самые нагруженные поиски остаются медленными.

Начните с продакшн‑данных

Опирайтесь на то, что люди действительно делают на странице поиска. Выбор индексов до запуска обычно основан на догадках. Реальный трафик быстро меняет картину. Пользователи могут игнорировать половину фильтров, массово выбирать один ценовой диапазон и сортировать по «новизне» гораздо чаще, чем кто‑то ожидал.

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

Медленные запросы важны, но объём имеет такое же значение. Запрос, который работает на 40 мс медленнее и выполняется 50 000 раз в день, заслуживает больше внимания, чем редкий поиск, который раз в час создаёт пик. Отделяйте обычные пути от крайних случаев до того, как лезть в индексы.

Небольшой рабочий набор обычно достаточен:

  • топ сочетаний фильтров по количеству
  • топ порядков сортировки для каждого частого сочетания
  • примеры медленных запросов по каждому паттерну
  • медиана и p95 задержки по паттерну
  • количество строк, просканированных vs возвращённых

Последний показатель экономит много напрасных усилий. Если поиск возвращает 20 строк, но сканирует 200 000, база явно говорит: текущий индекс плохо соответствует паттерну фильтра и сортировки.

Используйте продакшн‑инструменты, которым вы уже доверяете. Логи запросов полезны. Логи медленных запросов — ещё полезнее. Если вы уже измеряете задержки базы в Grafana или трассируете запросы в Sentry, вытащите и эти примеры и сопоставьте их с реальными действиями в поиске. Тут удобно начинать: настройка проще, когда числа взяты из живого поведения, а не из теории.

Цель этого шага проста: сократить хаотичный поток запросов до короткого списка паттернов поиска, которые действительно стоит исправить. Без такого списка команды будут днями настраивать поиски, которыми никто не пользуется, и упустят сортировку, которая съедает CPU весь день.

Превратите сырые логи в понятные паттерны поиска

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

Начните с группировки запросов по фильтрам, которые пользователи действительно объединяют. Сначала игнорируйте редкие уникальные комбинации. Если 38% поисков используют категория + диапазон цен, а 24% — статус + диапазон дат, это лучшие кандидаты, чем длинный хвост из пользовательских миксов, появляющихся дважды в неделю.

Для каждого запроса отслеживайте четыре вещи:

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

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

Выбор сортировки меняет картину больше, чем многие думают. Фильтр по статусу сам по себе может выглядеть дешёвым, но статус + сортировка по created_at ведёт себя очень иначе, чем статус + сортировка по цене. Если пользователи почти всегда комбинируют один набор фильтров с одной сортировкой, рассматривайте это как единый паттерн поиска.

Затем ранжируйте паттерны. Только трафик недостаточен. Запрос, который выполняется 500 раз в день и добавляет 2 секунды ожидания, может вредить больше, чем запрос, выполняющийся 5000 раз и возвращающийся за 40 мс. Поставьте рядом частоту и задержку, и дорогие паттерны быстро всплывут наверх.

Вот где настройка поиска становится управляемой. Вы перестаёте настраивать под догадки до запуска и начинаете настраивать под запросы, которые пользователи действительно выполняют.

Посмотрите, как база отвечает на эти запросы сейчас

Медленная страница поиска может выглядеть нормально в разработке, потому что маленькие таблицы скрывают плохие планы запросов. В продакшн это не так. Возьмите самые частые запросы поиска и выполните EXPLAIN или EXPLAIN ANALYZE для этих точных запросов.

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

Для каждого нагруженного запроса зафиксируйте несколько фактов:

  • сколько строк база просканировала
  • сколько строк вернула
  • использует ли индекс или читает всю таблицу
  • добавляет ли она отдельный шаг сортировки

Разрыв между просканированными и возвращёнными строками показывает, где расходуется время. Если запрос сканирует 60 000 строк, чтобы вернуть 20, значит индекс, скорее всего, покрывает только часть фильтра. Иногда индекс помогает при фильтрации, но база всё равно читает большой набор, прежде чем применит LIMIT.

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

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

Сохраните по одному плану для каждого частого сочетания фильтров и сортировки. Эти снимки пригодятся позже, когда вы будете тестировать изменения индексов и смотреть, уменьшилось ли число просканированных строк.

Меняйте индексы по шагам

Get a Second Set of Eyes
Have Oleg review your busiest search patterns before you ship new indexes.

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

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

Простой порядок действий работает хорошо:

  1. Найдите в продакшн наиболее загруженную пару фильтр+сорт.
  2. Постройте индекс, сначала включив колонки точных совпадений.
  3. Добавляйте колонки сортировки только если их часто используют и это оправдывает дополнительный размер индекса.
  4. Снова протестируйте после изменения.
  5. Удалите дублирующие или близкие индексы, когда новый индекс покрывает ту же задачу.

Порядок колонок важнее, чем многие думают. Если запрос фильтрует category = 'books' и status = 'active', а потом спрашивает price > 20, база обычно лучше использует индекс, когда колонки точных совпадений идут первыми, а диапазон — позже. Если начать с диапазонной колонки, индекс часто помогает меньше.

Колонки сортировки требуют той же дисциплины. Если пользователи сортируют по created_at в 80% случаев, добавление этой колонки может сильно сократить время ответа. Если только несколько людей сортируют по рейтингу раз в день, лишний индекс может стоить дороже, чем экономить.

Предположим, сейчас на странице используется индекс (status, brand, created_at), но в продакшн люди чаще ищут по (status, category) и сортируют по price. В таком случае индекс (status, category, price) может гораздо лучше обслуживать важный трафик. После тестирования старый индекс может оказаться балластом.

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

Простой пример после запуска

Небольшой каталог часто стартует с простого пути: просмотр по категории. Команда добавляет индекс на category_id, и страница кажется быстрой при небольшом трафике.

Через месяц люди ведут себя как настоящие покупатели. Они открывают категорию, ставят «в наличии», выставляют потолок цены и сортируют по новизне. Страница продолжает работать, но время ответа растёт, потому что базе теперь приходится делать лишнюю работу после нахождения строк по категории.

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

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

Часто правильным решением будет один составной индекс, построенный для пути, которым чаще всего пользуются, например category_id, in_stock, created_at DESC. Это позволяет базе зайти в меньший срез товаров и читать их в порядке, который уже нужен странице. Если многие пользователи также ставят максимальную цену, база будет проверять price уже на этом меньшем наборе, а не по всей категории.

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

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

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

Ошибки, которые тратят время впустую

Help for Search at Scale
Improve search performance without piling on duplicate indexes and write cost.

Один из самых быстрых путей к пустой трате усилий — считать каждый фильтр в UI поводом для нового индекса. На странице поиска может быть десять чекбоксов, три диапазона дат и две опции сортировки. Это не значит, что нужен индекс для каждого поля.

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

Тестовые данные в staging — ещё одна частая ошибка. Тестовые наборы часто аккуратны, маленьки и равномерны. Продакшн — грязный. Одно значение фильтра может соответствовать 40% строк, другое — 12 записям, а одна сортировка может доминировать почти везде.

Эта разница важна. Запрос, который на staging выглядит быстрым, может стать медленным в день реального трафика с широкими фильтрами, пустыми поисками и неравномерными данными.

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

Проверьте несколько реальных случаев вместо одного:

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

Эти случаи часто имеют одинаковую форму SQL, но ведут себя очень по‑разному.

Слишком много индексов вредят не только скорости чтения. Каждая вставка, обновление и удаление поддерживает все индексы. На активном продукте эта цена быстро накапливается. Часто экономят 80 мс на одном поиске и молча добавляют сотни миллисекунд к операциям записи.

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

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

Чеки, которые нужно прогнать до и после каждого изменения

Tune Before Costs Grow
Review slow searches, count queries, and write trade-offs with an experienced CTO.

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

Простое сравнение «до/после» спасает от многих домыслов. Если запрос упал с 900 мс до 120 мс — это очевидно. Если один фильтр стал быстрее, а остальные остались на месте, это тоже будет видно.

Ваша базовая линия не должна быть сложной:

  • топ паттернов поиска по реальному трафику
  • время ответа до изменения
  • план запроса до изменения
  • текущий размер индекса

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

Потом подтвердите, что база выбрала ожидаемый план. Смотрите реальный план запроса, а не только текст SQL. Нужно увидеть, что приложение использует новый индекс, избегает лишней сортировки и перестаёт сканировать в разы больше строк, чем нужно. Это важно, потому что небольшое изменение SQL со стороны приложения или ORM может заставить планировщик выбрать другой путь.

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

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

Держите чек простой: быстрее ли стали распространённые поиски, приемлемы ли скорости записи, контролируем ли рост индекса и совпадает ли план запроса с ожидаемым в продакшн. Если что‑то из этого не так — корректируйте снова, а не наращивайте ещё один индекс.

Что делать дальше

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

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

Логи должны помогать людям, а не только машинам. Записывайте набор фильтров, выбор сортировки, количество результатов, время запроса и как часто каждый паттерн встречается. Давайте понятные имена. «category + price asc» читается проще, чем сырая куча параметров. Когда команда видит паттерны с первого взгляда, решения принимаются быстрее.

Делайте каждую итерацию по индексам небольшой. Выберите один‑два паттерна поиска, которые часто встречаются или стоят дорого, измените индекс, измерьте снова и остановитесь, если выигрыш маленький. Узкая правка обычно лучше полной переделки, особенно когда страница меняется каждую пару недель.

Практичный рутинный план выглядит так:

  • регулярно просматривать логи поиска
  • группировать реальные запросы в повторяющиеся паттерны фильтров и сортировок
  • менять индексы только для паттернов с достаточным трафиком или явным замедлением
  • сравнивать время запросов и стоимость записей до и после каждого изменения

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

Если нужен второй взгляд, Oleg Sotnikov на oleg.is работает с командами над практической пост‑запусковой настройкой и может помочь просмотреть паттерны продакшн‑запросов перед тем, как вы начнёте добавлять индексы.

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