MongoDB 批量写入多个 updateOne 与 updateMany

Haf*_*fez 4 mongodb database-performance

我在某些情况下构建了 bulkWrite 操作,其中某些文档具有相同的update对象,合并过滤器并updateMany使用这些过滤器发送一个而不是多个updateOnes是否有任何性能优势bulkWrite

使用普通方法时,使用updateMany多个updateOnes显然更好,但是对于bulkWrite,因为它是一个命令,所以选择一个比另一个更重要吗?

例子:

我有 20 万个文档需要更新,status所有 20 万个文档总共有 10 个唯一字段,所以我的选项是:

解决方案:

A)发送一个包含 10 个updateMany操作的单个 bulkWrite ,这些操作中的每一个都会影响 20K 文档。

B)发送一个带有 200K 的单个 bulkWrite,updateOne每个操作保存其过滤器和status.

正如@AlexBlex 指出的那样,我必须注意使用相同的过滤器意外更新多个文档,在我的情况下,我_id用作我的过滤器,因此意外更新其他文档在我的情况下不是问题,但绝对值得一看在考虑updateMany选项时。

谢谢@AlexBlex。

Haf*_*fez 6

简短的回答:

使用updateMany速度至少快两倍,但可能会意外更新比预期更多的文档,请继续阅读以了解如何避免这种情况并获得性能优势。

长答案:

我们进行了以下实验以了解答案,步骤如下:

  1. 创建一个bankaccounts mongodb 集合,每个文档只包含一个字段(余额)。
  2. 将 100 万个文档插入到bankaccounts集合中。
  3. 随机化所有 100 万个文档在内存中的顺序,以避免使用以相同序列插入的 id 对数据库进行任何可能的优化,模拟真实世界的场景。
  4. 从具有 0 到 100 之间的随机数的文档中为 bulkWrite 构建写入操作。
  5. 执行批量写入。
  6. 记录 bulkWrite 花费的时间。

现在,实验进行到第 4 步。

在实验的一个变体中,我们构建了一个由 100 万个updateOne操作组成的数组,每个操作updateOne都有filter一个文档,以及它各自的“更新对象”。

在第二个变体中,我们构建了 100 个updateMany操作,每个操作包括filter10K 个文档 ID,以及它们各自的update.

结果: updateMany多个文档 id 比多个updateOnes快 243% ,但这不能在任何地方使用,请阅读“风险”部分以了解何时应该使用它

详细信息: 我们为每个变体运行了 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;
}
Run Code Online (Sandbox Code Playgroud)

输出

$ node 65831219.js
Write operations took 20.803 seconds with `buildUpdateManyWriteOperations`.
Write operations took 50.84 seconds with `buildUpdateOneWriteOperations`.
----------------------------------------------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

使用MongoDB 4.0.4运行测试。


Gib*_*bbs 5

在高层次上,如果您有相同的更新对象,那么您可以这样做updateMany而不是bulkWrite

原因:

bulkWrite旨在向服务器发送多个不同的命令,如此处所述

如果您有相同的更新对象,updateMany则最适合。

表现:

如果bulkWrite中有10k个更新命令,它将在内部以批处理方式执行。可能会影响执行时间

有关批处理的参考文献中的精确行:

每组操作最多可以有 1000 个操作。如果某个组超过此限制,MongoDB 会将该组划分为 1000 或更少的较小组。例如,如果批量操作列表包含 2000 个插入操作,MongoDB 将创建 2 个组,每个组包含 1000 个操作。

谢谢@亚历克斯