MongoDB bulkWrite 多个 updateOne 和 updateMany 的区别

13
我有一些情况需要构建bulkWrite操作,其中一些文档具有相同的update对象,将这些过滤器合并并发送一个带有这些过滤器的updateMany是否比在同一个bulkWrite中使用多个updateOne操作性能更好?
显然,在正常方法中使用updateMany比使用多个updateOne更好,但是在bulkWrite中,由于它是单个命令,因此是否选择一种而不是另一种会有任何显着的优势?
例如:我有200k篇文档需要更新,对于所有200K文档,我有10个唯一的status字段。因此,我的选择是:
A) 发送一个包含10个updateMany操作的单个bulkWrite,每个操作将影响20K个文档。
B) 发送一个包含200K个updateOne操作的单个bulkWrite,每个操作都保留其过滤器和status
正如@AlexBlex所指出的那样,我必须注意意外地使用相同的过滤器更新多个文档,在我的情况下,我使用_id作为我的过滤器,因此意外更新其他文档不是我的问题,但在考虑updateMany选项时绝对要注意这一点。
谢谢@AlexBlex。

更新一个和更新多个是不同的操作。一个不能直接替代另一个。 - D. SM
请纠正我,如果我们在过滤器中使用诸如“_id”之类的唯一字段,它们应该相互替换,对吗? - Hafez
2个回答

24

简短回答:

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

长回答:

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

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

现在,实验在第4步中。

在实验的一个变体中,我们构建了一个由100万个updateOne操作组成的数组,每个updateOne都包含单个文档的filter和其相应的update对象。

在第二个变体中,我们构建了100个updateMany操作,每个操作包括10K文档ID的filter和其相应的update

结果: 使用多个文档ID的updateMany比多个updateOne快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;
}

输出

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

测试使用MongoDB版本4.0.4。


好的。减少操作数量可以提高性能。 - D. SM

5

高层次上,如果您具有相同的更新对象,则可以使用updateMany而不是bulkWrite

原因:

bulkWrite被设计为向服务器发送多个不同的命令,如此处所述

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

性能:

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

关于分批处理的引用确切行:

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

感谢@Alex


我明白了。我的使用情况是我有10万个文档,每个文档都有10K的相同更新对象,因此bulkWrite似乎是我最好的选择。我的问题是,使用10个“updateMany”* 10K筛选器中的ID是否比100K个“updateOne”更好(显着?)。 - Hafez
显然,在bulkWrite中使用10个updateMany是最好的选择,不要忘记网络往返时间和内部批处理。 - Gibbs
对你来说显而易见的并不是对每个人都显而易见的。此外,我正在比较单个bulkWrite中的10 * 10K updateMany和100k updateOne,在这两种情况下应该只有一个网络往返,对吗? - Hafez
1
值得一提的是,批量操作每个批次限制为1000个操作。https://docs.mongodb.com/manual/reference/method/Bulk/ 在单个批量更新中发送10k个更新将导致10个“命令”。另一个非常重要的事情要记住的是过滤器。它可能会匹配更多的文档在updateMany中,这比您预期的要多。强烈建议OP发布文档和查询示例供社区验证。 - Alex Blex
1
@AlexBlex 是的,我已经进行了一个小时的实验,结果显示updateManyupdateOne性能提高了2.5倍。我将详细发布结果以及实验步骤供人们参考。 - Hafez
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接