MongoDB bulkWrite 多个 updateOne 与 updateMany
MongoDB bulkWrite multiple updateOne vs updateMany
我有这样的情况,我构建了一些文档具有相同 update
对象的 bulkWrite 操作,合并过滤器并发送一个 updateMany
与这些过滤器而不是多个 updateOne
s 在同一个 bulkWrite
?
在使用普通方法时,使用 updateMany
显然比使用多个 updateOne
更好,但是对于 bulkWrite,因为它是一个命令,所以优先使用一个命令比另一个命令有什么显着的好处吗?
示例:
我有 20 万个文档需要更新,对于所有 20 万个文档,我总共有 10 个唯一 status
字段,所以我的选择是:
解决方案:
A) 发送一个包含 10 个 updateMany
操作的 bulkWrite,每个操作都会影响 20K 个文档。
B) 发送一个 200K 的 bulkWrite updateOne
每个操作保持其过滤器和 status
.
正如@AlexBlex 指出的那样,我必须注意使用相同的过滤器意外更新多个文档,在我的例子中我使用 _id
作为我的过滤器,所以意外更新其他文档不是问题就我而言,但在考虑 updateMany
选项时绝对需要注意。
谢谢@AlexBlex。
在高级别,如果您有相同的更新对象,那么您可以执行 updateMany
而不是 bulkWrite
原因:
bulkWrite
旨在向服务器发送多个不同的命令,如前所述 here
如果您有相同的更新对象,updateMany
最适合。
性能:
如果你在bulkWrite中有10k条更新命令,它会在内部批量执行。可能影响执行时间
参考文献中有关批处理的确切行数:
Each group of operations can have at most 1000 operations. If a group exceeds this limit, MongoDB will divide the group into smaller groups of 1000 or less. For example, if the bulk operations list consists of 2000 insert operations, MongoDB creates 2 groups, each with 1000 operations.
感谢@Alex
简答:
使用 updateMany
至少快两倍,但可能会意外更新比预期更多的文档,请继续阅读以了解如何避免这种情况并获得性能优势。
长答案:
我们运行下面实验一下就知道答案了,步骤如下:
- 创建一个bankaccountsmongodb集合,每个文档只包含一个字段(余额)。
- 将 100 万份文档插入 bankaccounts 集合。
- 随机化所有 100 万个文档在内存中的顺序,以避免使用以相同顺序插入的 ID 对数据库进行任何可能的优化,模拟真实场景。
- 从 运行dom 编号介于 0 和 100 之间的文档构建 bulkWrite 的写入操作。
- 执行批量写入。
- 记录 bulkWrite 所花费的时间。
现在实验进行到第4步
在实验的一个变体中,我们构建了一个由 100 万个 updateOne
操作组成的数组,每个 updateOne
对单个文档及其各自的 `update 对象具有 filter
。
在第二个变体中,我们构建了 100 个 updateMany
操作,每个操作包括 filter
用于 10K 个文档 ID,以及它们各自的 update
.
结果:
updateMany
具有多个文档 ID 比多个 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.
我有这样的情况,我构建了一些文档具有相同 update
对象的 bulkWrite 操作,合并过滤器并发送一个 updateMany
与这些过滤器而不是多个 updateOne
s 在同一个 bulkWrite
?
在使用普通方法时,使用 updateMany
显然比使用多个 updateOne
更好,但是对于 bulkWrite,因为它是一个命令,所以优先使用一个命令比另一个命令有什么显着的好处吗?
示例:
我有 20 万个文档需要更新,对于所有 20 万个文档,我总共有 10 个唯一 status
字段,所以我的选择是:
解决方案:
A) 发送一个包含 10 个 updateMany
操作的 bulkWrite,每个操作都会影响 20K 个文档。
B) 发送一个 200K 的 bulkWrite updateOne
每个操作保持其过滤器和 status
.
正如@AlexBlex 指出的那样,我必须注意使用相同的过滤器意外更新多个文档,在我的例子中我使用 _id
作为我的过滤器,所以意外更新其他文档不是问题就我而言,但在考虑 updateMany
选项时绝对需要注意。
谢谢@AlexBlex。
在高级别,如果您有相同的更新对象,那么您可以执行 updateMany
而不是 bulkWrite
原因:
bulkWrite
旨在向服务器发送多个不同的命令,如前所述 here
如果您有相同的更新对象,updateMany
最适合。
性能:
如果你在bulkWrite中有10k条更新命令,它会在内部批量执行。可能影响执行时间
参考文献中有关批处理的确切行数:
Each group of operations can have at most 1000 operations. If a group exceeds this limit, MongoDB will divide the group into smaller groups of 1000 or less. For example, if the bulk operations list consists of 2000 insert operations, MongoDB creates 2 groups, each with 1000 operations.
感谢@Alex
简答:
使用 updateMany
至少快两倍,但可能会意外更新比预期更多的文档,请继续阅读以了解如何避免这种情况并获得性能优势。
长答案:
我们运行下面实验一下就知道答案了,步骤如下:
- 创建一个bankaccountsmongodb集合,每个文档只包含一个字段(余额)。
- 将 100 万份文档插入 bankaccounts 集合。
- 随机化所有 100 万个文档在内存中的顺序,以避免使用以相同顺序插入的 ID 对数据库进行任何可能的优化,模拟真实场景。
- 从 运行dom 编号介于 0 和 100 之间的文档构建 bulkWrite 的写入操作。
- 执行批量写入。
- 记录 bulkWrite 所花费的时间。
现在实验进行到第4步
在实验的一个变体中,我们构建了一个由 100 万个 updateOne
操作组成的数组,每个 updateOne
对单个文档及其各自的 `update 对象具有 filter
。
在第二个变体中,我们构建了 100 个 updateMany
操作,每个操作包括 filter
用于 10K 个文档 ID,以及它们各自的 update
.
结果:
updateMany
具有多个文档 ID 比多个 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.