Оптимизация MongoDB-запросов: explain, индексы, aggregation pipeline

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

Оптимизация MongoDB: где запросы теряют скорость

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

Первое место потерь — сканирование коллекции вместо индексного доступа. explain() показывает COLLSCAN там, где ожидался IXSCAN, и дальше каждая новая порция документов делает чтение дороже. Бывает, индекс формально есть, но составной собран в неверном порядке — равенство, сортировка, диапазон — и планировщик его просто не берёт.

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

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

Что даёт настройка индексов и планов выполнения

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

Дальше перестраиваем индексы под фактические шаблоны запросов. Составной индекс собирается в порядке «равенство → сортировка → диапазон», чтобы фильтр и сортировка закрывались одним проходом; там, где приложение выбирает только несколько полей, делаем его покрывающим — сервер отдаёт ответ без обращения к самим документам. Параллельно снимаем дублирующие и неиспользуемые индексы: каждый лишний индекс замедляет запись и раздувает рабочее множество, вытесняя нужные страницы из памяти. Для MongoDB это не разовая правка, а сверка с текущим профилем запросов.

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

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

Подходы к ускорению запросов: сравнение вариантов

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

Вариант Когда уместен Ограничения
Точечный индекс по одному полю Фильтр по высокоселективному полю, в выборке единицы документов Сортировка по второму полю уходит в память и на большом наборе рвёт запрос
Составной индекс Устойчивый набор условий плюс сортировка по тем же полям Порядок полей жёсткий: запрос с другим набором полей индекс не задействует
Покрывающий индекс Фильтр и проекция целиком лежат в индексе, документ не читается Растёт размер индекса и цена каждой записи; список полей зашит в индекс
Разделение чтения Отчётные и аналитические выборки, которые не должны мешать основной нагрузке Отставание вторичных узлов и нестрогая согласованность; сам запрос не ускоряется
Разнесение данных Тяжёлые вложенные массивы, которые читаются редко, но раздувают документ Усложняются запись и согласованность, добавляются дополнительные обращения

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

Этапы работ: от аудита запросов до передачи настроек

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

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

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

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

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

  5. Проверка под нагрузкой — прогоняем изменённый вариант на реалистичном профиле: та же доля чтения и записи, те же конкурирующие операции. Сравниваем план, число просмотренных документов и время ответа до и после.

  6. Фиксация и передача настроек — отдаём перечень индексов с обоснованием, правила обращения к коллекциям, метрики для мониторинга и короткие инструкции, чтобы команда поддерживала результат без нас.

Индексы, схемы и регламенты: что остаётся в проекте

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

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

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

Кейс: ускорили каталог без переписывания логики

Что было

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

Что изменили

Разобрали план выполнения реальных запросов на MongoDB: видно, где идёт полный проход по коллекции, а где индекс есть, но не задействован из-за порядка полей. Собрали составные индексы под фактические комбинации фильтров: категория и наличие — ведущими полями, цена и дата — хвостом под сортировку. В конвейере перенесли фильтрацию до соединения со связанными коллекциями и оставили проекцию только нужных полей — во вторую стадию попадают десятки документов вместо всего каталога. Логику выдачи и формат ответов не трогали.

Что стало

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

Что делать, если оптимизация MongoDB не дала эффекта

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

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

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

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

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

Технические сомнения: отвечаем на частые вопросы

Оптимизация запросов упирается не только в сам запрос, но и в то, что вокруг: как правки отразятся на записи, нужно ли трогать схему документов, что изменится при шардировании и где проверять результат. Ниже — вопросы, которые чаще всего приходят на этапе оценки.

Не замедлят ли новые индексы запись?

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

Можно ли обойтись без изменения схемы?

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

Что будет при шардировании?

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

На каком окружении проверяем результат?

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

Обсудим задачу: вернёмся с оценкой и планом работ

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

  • Список проблемных операций: время отклика и частота обращений за сутки.
  • Вывод explain по ним — план выполнения, доля отсечённых документов, использованный индекс.
  • Схема коллекций: типы полей, объёмы, распределение по репликам.
  • Текущие индексы и статистика обращений к ним — сразу видно, какие работают вхолостую.
  • Профиль нагрузки: соотношение чтений и записей, пиковые часы, характер всплесков.
  • Ограничения контура: окно обслуживания, доступные реплики для тяжёлых задач, требования к согласованности данных.

В ответ придёт не перечень общих советов, а план этапов с приоритетами: что даёт эффект сразу, что требует перестроения индексов и подготовки, какими метриками замеряем результат. Дальше согласуем порядок и стартуем с участка, где отдача максимальна — опишите задачу, и вернёмся с оценкой.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка логотипа компании B2B Advance
    Разработка логотипа компании B2B Advance
    815
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139