MongoDB памятка по оптимизации производительности

Здесь я написал несколько вещей, в основном связанных с индексированием данных в MongoDB, на которых хотел бы заострить внимание.

Не используйте $ne, $nin

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

Честно говоря, не знаю зачем вообще понадобились эти операторы – в таком виде они просто опасны.

FAQ: Indexes — MongoDB Manual

Используйте ^ в регексе, не используйте case insensitive mode

В этом случае, ваши поисковые запросы будут проиндексированы. В противном случае regex будет работать как "LIKE" операция в SQL, те. за линейное время. Если вам нужно что-то большее или используйте full-text search ($text) индексы или используйте специальные инструменты (mongodb atlas, elastic search, sphynx).

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

$regex — MongoDB Manual
pattern matching on strings in MongoDB 7.0

Ограничивайте временные интервалы в условии запроса count. Сохраняйте статистики отдельно

Точно так же, ограничивайте параметры запроса count в критерии, по которым может вернуть большое количество данных.

Запросы типа count или sum будут молотить все данные, им не поставить лимит.

Лучше просто отдать "фигу" по интервалу в год, чем положить в один "прекрасный" день базу.

Для того чтобы на лету получать count или sum или другие агрегации используйте вертикальные БД, такие, как ClickHouse. Они для этого, собственно, и сделаны.

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

ESR rule of thumb

Порядок полей в индексе важен:

  • во-первых, добавьте в индекс поле, по которому происходит сравнение на равенство (Equality)
  • во-вторых, добавьте в индекс поле, по которому происходит сортировка (Sort)
  • в-третьих, добавьте в индекс поле, по которому происходит сравнение на больше/меньше (Range)

Не забывайте, что индексы нужно создавать с учетом порядка сортировки в запросе (по возрастанию, по убыванию).

The ESR (Equality, Sort, Range) Rule — MongoDB Manual
Performance Best Practices: Indexing | MongoDB Blog
Best practices for delivering performance at scale with MongoDB. Learn about the importance of indexing and tools to help you select the right indexes.

Делайте "базовый" индекс в мультитенантных системах

Если по какой-то причине у вас не сработает ваш индекс, но который вы рассчитываете, базовый индекс не даст сделать запрос по всем данным. Да это будет линейный проход, но по некоторому ограниченному набору. Это не даст вашей системе упасть, только замедлит её до тех пор, пока вы это не почините.

Следите за использованием ваших индексов

В mongodb есть ограничение на количество индексов для одной коллекции (не более 64-х). Так же есть оператор $indexStats, который дает понимание используются ли ваши индексы и как часто.

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

MongoDB Limits and Thresholds — MongoDB Manual
Measure Index Use — MongoDB Manual

Настройте графану на анализ логов slow queriues

Настройте уведомления на 3-х секундные запросы. Они точно нуждаются в улучшении.

Для того чтобы показать самые "тяжелые" запросы из slow logs (чтобы затем проиндексировать их)  полезен следующий скрипт:

docker logs --since '3h' mongodb | sed -n 's/^\(.*\) I COMMAND  \[conn[0-9]\+\] command \(.*\) protocol:op_query \([0-9]\+\)ms$/\1|\2|\3 ms/p' | awk -F'|' '{if ($3+0 > 5000) print($1 " " $2 " " $3);}' | grep -v update

Он выводит для read запросы, выполнявшиеся 5 секунд и более за последние 3 часа. Пороговое значение времени и объем логов, в котором происходит поиск можно менять в зависимости от вашего случая.

Не используйте ORM, которые процессят данные

Можно разделить ORM на 2 типа:

  1. Те в которых данные процесятся по всему объему по умолчанию (EFCore, mongoose, prisma.js)
  2. Те, в которых этого не происходит (dapper, sqlkata, mongodb native client)

В первом случае, рано или поздно возникнут проблемы с производительностью. Конечно, есть функции/методы, которые предотвращают стандартное поведение и не дают выполнять высокозатратные операции обработки данных. Но при их использовании вы столкнетесь с необходимостью рефакторить код,  поскольку данные будут видоизменены. И так в каждом месте, в котором есть проблемы с производительностью.

Во втором случае, таких проблем нет.

Для mongodb ситуация осложняется. Если в SQL принято явно указывать поля в SELECT, то в mongodb это сильно опционально. А схемы с развитием бизнеса растут и рано или поздно, если вы используете, например, mongoose, вы столкнетесь с проблемой производительности.

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