MongoDB bulkWrite несколько обновлений один против updateMany

#mongodb #database-performance

#mongodb #база данных-производительность

Вопрос:

У меня есть случаи, когда я создаю операции bulkWrite, когда некоторые документы имеют один и тот же update объект, есть ли какое-либо преимущество в производительности при объединении фильтров и отправке одного updateMany с этими фильтрами вместо нескольких updateOne s в одном и том же bulkWrite ?

Очевидно updateMany , что при использовании обычных методов лучше использовать более нескольких updateOne s, но с bulkWrite, поскольку это одна команда, есть ли какие-либо существенные выгоды от предпочтения одной над другой?

Пример:

У меня есть 200 тыс. документов, которые мне нужно обновить, у меня всего 10 уникальных status полей для всех 200 тыс. документов, поэтому мои варианты:

Решения:

A) Отправьте одну единственную bulkWrite с 10 updateMany операциями, и каждая из этих операций повлияет на 20 тыс. документов.

Б) Отправьте одну единственную bulkWrite с 200 updateOne КБ каждой операции, содержащей свой фильтр и status .

Как отметил @AlexBlex, я должен следить за случайным обновлением более одного документа с помощью одного и того же фильтра, в моем случае я использую _id его в качестве фильтра, поэтому случайное обновление других документов в моем случае не вызывает беспокойства, но, безусловно, стоит обратить внимание при рассмотрении этого updateMany варианта.

Спасибо @AlexBlex.

Комментарии:

1. Обновление одного и обновление многих — это разные операции. Одно не является прямой заменой другого.

2. Поправьте меня, если я ошибаюсь, но если мы используем уникальное поле, например, _id в фильтре, они должны заменять друг друга, правильно?

Ответ №1:

Короткий ответ:

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

Длинный ответ:

Мы провели следующий эксперимент, чтобы узнать ответ на этот вопрос, следующие шаги:

  1. Создайте коллекцию bankaccounts mongodb, каждый документ содержит только одно поле (баланс).
  2. Вставьте 1 миллион документов в коллекцию bankaccounts.
  3. Рандомизируйте порядок в памяти всех 1 миллиона документов, чтобы избежать любых возможных оптимизаций из базы данных с использованием идентификаторов, которые вставляются в той же последовательности, имитируя реальный сценарий.
  4. Создайте операции записи для bulkWrite из документов со случайным числом от 0 до 100.
  5. Выполните bulkWrite.
  6. Регистрируйте время, затраченное на выполнение bulkWrite.

Теперь эксперимент находится на 4-м шаге.

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

Во втором варианте мы создаем 100 updateMany операций, каждая из которых включает filter идентификаторы документов 10K и их соответствующие update .

Результаты:
updateMany с несколькими идентификаторами документов на 243% быстрее, чем с несколькими updateOne s, это не может быть использовано везде, хотя, пожалуйста, прочитайте раздел «Риск», чтобы узнать, когда его следует использовать.

Подробности: Мы запускали скрипт 5 раз для каждого варианта, подробные результаты следующие: С updateOne: в среднем 51,28 секунды. С updateMany: в среднем 21,04 секунды.

Риск: как уже указывали многие люди, updateMany не является прямой заменой updateOne , поскольку он может неправильно обновлять несколько документов, когда мы намеревались действительно обновить только один документ. Этот подход допустим только при использовании уникального поля, такого как _id или любого другого уникального поля, если фильтр зависит от полей, которые не являются уникальными, несколько документов будут обновлены, и результаты не будут эквивалентными.

65831219.js

 // 65831219.js
'use strict';
const mongoose = require('mongoose');
const { Schema } = mongoose;

const DOCUMENTS_COUNT = 1_000_000;
const UPDATE_MANY_OPERATIONS_COUNT = 100;
const MINIMUM_BALANCE = 0;
const MAXIMUM_BALANCE = 100;
const SAMPLES_COUNT = 10;

const bankAccountSchema = new Schema({
  balance: { type: Number }
});

const BankAccount = mongoose.model('BankAccount', bankAccountSchema);

mainRunner().catch(console.error);

async function mainRunner () {
  for (let i = 0; i < SAMPLES_COUNT; i  ) {
    await runOneCycle(buildUpdateManyWriteOperations).catch(console.error);
    await runOneCycle(buildUpdateOneWriteOperations).catch(console.error);
    console.log('-'.repeat(80));
  }
  process.exit(0);
}

/**
 *
 * @param {buildUpdateManyWriteOperations|buildUpdateOneWriteOperations} buildBulkWrite
 */
async function runOneCycle (buildBulkWrite) {
  await mongoose.connect('mongodb://localhost:27017/test', {
    useNewUrlParser: true,
    useUnifiedTopology: true
  });

  await mongoose.connection.dropDatabase();

  const { accounts } = await createAccounts({ accountsCount: DOCUMENTS_COUNT });

  const { writeOperations } = buildBulkWrite({ accounts });

  const writeStartedAt = Date.now();

  await BankAccount.bulkWrite(writeOperations);

  const writeEndedAt = Date.now();

  console.log(`Write operations took ${(writeEndedAt - writeStartedAt) / 1000} seconds with `${buildBulkWrite.name}`.`);
}



async function createAccounts ({ accountsCount }) {
  const rawAccounts = Array.from({ length: accountsCount }, () => ({ balance: getRandomInteger(MINIMUM_BALANCE, MAXIMUM_BALANCE) }));
  const accounts = await BankAccount.insertMany(rawAccounts);

  return { accounts };
}

function buildUpdateOneWriteOperations ({ accounts }) {
  const writeOperations = shuffleArray(accounts).map((account) => ({
    updateOne: {
      filter: { _id: account._id },
      update: { balance: getRandomInteger(MINIMUM_BALANCE, MAXIMUM_BALANCE) }
    }
  }));

  return { writeOperations };
}

function buildUpdateManyWriteOperations ({ accounts }) {
  shuffleArray(accounts);
  const accountsChunks = chunkArray(accounts, accounts.length / UPDATE_MANY_OPERATIONS_COUNT);
  const writeOperations = accountsChunks.map((accountsChunk) => ({
    updateMany: {
      filter: { _id: { $in: accountsChunk.map(account => account._id) } },
      update: { balance: getRandomInteger(MINIMUM_BALANCE, MAXIMUM_BALANCE) }
    }
  }));

  return { writeOperations };
}


function getRandomInteger (min = 0, max = 1) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return min   Math.floor(Math.random() * (max - min   1));
}

function shuffleArray (array) {
  let currentIndex = array.length;
  let temporaryValue;
  let randomIndex;

  // While there remain elements to shuffle...
  while (0 !== currentIndex) {

    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }

  return array;
}

function chunkArray (array, sizeOfTheChunkedArray) {
  const chunked = [];

  for (const element of array) {
    const last = chunked[chunked.length - 1];

    if (!last || last.length === sizeOfTheChunkedArray) {
      chunked.push([element]);
    } else {
      last.push(element);
    }
  }
  return chunked;
}
 

Вывод

 $ node 65831219.js
Write operations took 20.803 seconds with `buildUpdateManyWriteOperations`.
Write operations took 50.84 seconds with `buildUpdateOneWriteOperations`.
----------------------------------------------------------------------------------------------------
 

Тесты были выполнены с использованием MongoDB версии 4.0.4.

Комментарии:

1. Правильно. Уменьшение количества операций повышает производительность.

Ответ №2:

На высоком уровне, если у вас один и тот же объект обновления, вы можете сделать updateMany , а не bulkWrite

Причина:

bulkWrite предназначен для отправки нескольких разных команд на сервер, как указано здесь

Если у вас один и тот же объект обновления, updateMany лучше всего подходит.

Производительность:

Если у вас есть 10 тысяч команд обновления в bulkWrite, они будут выполняться пакетным способом внутри. Это может повлиять на время выполнения

Точные строки из ссылки о пакетной обработке:

Каждая группа операций может содержать не более 1000 операций. Если группа превышает это ограничение, MongoDB разделит группу на более мелкие группы по 1000 или менее. Например, если список массовых операций состоит из 2000 операций вставки, MongoDB создает 2 группы, каждая из которых содержит 1000 операций.

Спасибо @Alex

Комментарии:

1. Я понимаю. Мой вариант использования здесь заключается в том, что у меня есть 100 Тыс. документов, каждые 10 тыс. из которых используют один и тот же объект обновления, поэтому bulkWrite кажется моим лучшим вариантом здесь. Мой вопрос заключается в том, является ли 10 updateMany * 10K идентификаторов в фильтре (значительно?) лучше, чем 100 updateOne КБ с или нет.

2. Очевидно, что 10 updateMany в bulkWrite — лучший вариант, не забывайте о времени обхода сети и внутренней пакетной обработке.

3. То, что может быть очевидным для вас, не очевидно для всех. Кроме того, я сравниваю 10 * 10K updateMany в одном bulkWrite с 100k updateOne в одном bulkWrite, в обоих случаях это должен быть один сетевой переход туда и обратно, правильно?

4. Стоит отметить, что массовая операция ограничена 1000 операциями на партию docs.mongodb.com/manual/reference/method/Bulk Отправка 10 тысяч обновлений за одно массовое обновление приводит к получению 10 «команд». Еще одна очень важная вещь, о которой следует помнить, — это фильтр. Он может соответствовать большему количеству документов в updateMany, чем вы ожидаете. Настоятельно рекомендуется использовать OP для публикации примеров документов и запросов для проверки сообществом.

5. @AlexBlex Да, я проводил эксперимент в течение последнего часа, и результаты показывают увеличение производительности updateMany более чем в 2,5 раза updateOne . Я буду публиковать результаты в деталях, а также этапы эксперимента, с которыми люди могут повозиться.