Что вы можете сделать, чтобы ускорить запросы mongodb / mongoose?

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

1. Используйте простые запросы для операций GET

Вероятно, это лучшее, что вы можете сделать для повышения производительности запроса. Mongoose позволяет вам добавлять .lean() в конце ваших запросов, что значительно повышает производительность вашего запроса, возвращая простые объекты JSON вместо Mongoose Documents.

Из Mongoose docs on Lean

По умолчанию запросы Mongoose возвращают экземпляр класса Mongoose Document. Документы намного тяжелее обычных объектов JavaScript, потому что у них много внутреннего состояния для отслеживания изменений. Включение leanoption указывает Mongoose пропустить создание полного документа Mongoose и просто предоставить вам POJO.

Бережливый вариант указывает Mongoose пропустить увлажнение документов результатов. Это ускоряет запросы и снижает интенсивность использования памяти, но результирующие документы представляют собой простые старые объекты JavaScript (POJO), не документы Mongoose ».

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

  • Отслеживание изменений
  • Кастинг и проверка
  • Геттеры и сеттеры
  • Виртуальные машины (включая «id»)
  • save() функция

Поэтому обычно он оптимален для конечных точек GET и .find() операций, которые не используют .save() или виртуальные ресурсы.

2. Создавайте индивидуальные индексы для ваших запросов.

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

Вы также можете создавать составные индексы более чем одного свойства. Это полезно, если вы запрашиваете по нескольким полям. Допустим, у вас есть база данных и вы хотите найти всех вымерших животных, вы, вероятно, напишете такой запрос Model.find({type: “Animal”, status: “extinct"})

MongoDB должна будет просмотреть все документы, чтобы найти те, которые соответствуют этому критерию. Чтобы оптимизировать этот запрос, вы можете создать составной индекс для «типа» и «статуса», добавив ModelSchema.index({type: 1, status: 1}). Теперь MongoDB будет искать соответствующие документы.

1 или -1 указывают порядок сортировки свойств. Порядок, в котором поля добавляются в индекс, важен. Для более подробного объяснения индексов см. Https://mongoosejs.com/docs/guide.html#indexes и https://docs.mongodb.com/manual/indexes/.

3. Сведите к минимуму запросы к БД (по возможности избегайте .populate ())

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

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

Если ваш документ включает в себя массив ссылок на другие модели, и вы используете .populate () для объединения данных между коллекциями, использование .populate() потребует выполнения дополнительных запросов для получения фактических документов внутри этого массива для вас, так что это похоже на запуск дополнительного запрос для каждого идентификатора для каждого документа. Лучше использовать .aggregate() вместо .populate(), если это действительно необходимо.

4. Используйте .select (), чтобы выбрать определенные свойства, которые нужно вернуть.

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

Чтобы база данных не выполняла дополнительную работу по возврату этих полей, увеличивая размер возвращаемых документов, вы можете использовать mongoose .select() для включения / исключения полей, которые вы хотите, чтобы ваш запрос возвращал, в частности, следующим образом:

Model.find({type: "Animal"}).select({name: 1})

Совет: это очень хорошо работает, если вы используете GraphQL, поэтому вы знаете, какие именно поля запрашивал клиент, и можете выбирать эти поля из базы данных. Я написал еще одну статью об этом здесь



5. Параллельно запускайте операции с базами данных.

Распространенная ошибка, которую я вижу в коде NodeJS всякий раз, когда люди используют async / await, заключается в том, что люди запускают операции одну за другой, когда в этом нет необходимости. Например:

const user = new User({name: "bob"})
const post = new Post({title: "hello"})
await user.save()
await post.save()

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

const [user, post] = await Promise.all([user.save(), post.save()])

Хотя это может улучшить производительность на уровне API, мы все еще выполняем два запроса к базе данных, поэтому еще лучше использовать insertMany() илиbulkWrite(), если вы хотите выполнять несколько операций как пакет.

6. Кэширование / повторное использование соединений мангуста.

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

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

Давайте напишем несколько тестов производительности, чтобы увидеть результаты

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

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

Я написал запрос по-разному, комбинируя упомянутые выше методы, и попробовал их в двух разных коллекциях баз данных, которые заполнены одним и тем же набором данных. Одна коллекция имела указатель на свойство «возраст», а другая - нет. Результаты были измерены с использованием console.time() API, а тесты были запущены с использованием последних текущих версий NodeJS (v12.7.0) и mongoose (v5.6.7).

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

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

Теперь, когда база данных готова, давайте напишем наши запросы

Результаты и наблюдения производительности

Для 1000 пользователей
default_query: 135,646 мс
query_with_index: 168,763 мс
query_with_select: 27,781 мс
query_with_select_index: 55,686 мс < br /> Lean_query: 7,191 мс
Lean_with_index: 7,341 мс
Lean_with_select: 4,226 мс
Lean_select_index: 7,881 мс

С 10 тыс. пользователей в базе данных

default_query: 323.278 мс
query_with_index: 355.693ms
query_with_select: 212.403ms
query_with_select_index: 183.736ms
Lean_query: 92.935ms
Lean_with_index: 92.755ms
36,175 мс
Lean_select_index: 38,456 мс

При 100 000 пользователей в базе данных
default_query: 2425,857 мс
query_with_index: 2371,716 мс
query_with_select: 1580,393 мс
query_with_select_index: 1583,015 мс
Lean_query: 858,839 мс
Lean_with_index: 944,712 мс
Lean_with_select: 383,548 мс
Lean_select_index: 458,000 мс

Как видите, оптимизированная версия запроса, в которой используются .lean() .select() и Schema.index({}), была примерно в 10 раз быстрее, чем запрос по умолчанию, что является огромным выигрышем!

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

Вы можете найти скрипт, используемый для этих тестов, и даже попробовать его самостоятельно здесь https://github.com/khaledosman/mongo-performance-expirements