简短回答:
使用updateMany
至少快两倍,但可能会意外更新更多的文档,继续阅读以了解如何避免这种情况并获得性能优势。
长回答:
我们进行了以下实验来了解答案,以下是步骤:
- 创建一个名为bankaccounts的mongodb集合,每个文档只包含一个字段(余额)。
- 向bankaccounts集合插入100万个文档。
- 在内存中随机排列所有100万个文档的顺序,以避免数据库使用按相同顺序插入的ID进行任何可能的优化,从而模拟真实世界的情况。
- 使用介于0到100之间的随机数为批量写入构建编写操作。
- 执行批量写入。
- 记录批量写入所需的时间。
现在,实验在第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
'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);
}
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 (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
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。