bulkwrite中upsert需在每个操作内设upsert: true,但核心是filter必须唯一标识文档,否则易误插或漏更;filter应基于业务唯一键并配唯一索引,$setoninsert可保初始值不被覆盖。

bulkWrite 里怎么写 upsert 条件
直接在 bulkWrite 的每个操作里加 upsert: true,但关键不是“开开关”,而是 filter 必须能唯一标识文档——否则可能意外创建多条、或漏更新。MongoDB 不会自动用 update 字段推导条件,filter 和 update 完全解耦。
常见错误:把 update 对象当条件用,比如传 { $set: { status: "done" } } 却没写 filter,结果报错 "filter" must be specified;或者 filter 写成 { _id: null } 这类恒假值,导致全量 upsert(即反复插入新文档)。
-
filter应该基于业务唯一键,比如{ orderId: "ORD-123" }或{ userId: 1001, date: "2024-06-01" } - 如果想按多个字段组合 upsert,
filter就写成完整对象,不要省略任何参与判断的字段 - 避免在
filter里用$regex、$in等非精确匹配——bulkWrite 不支持对 upsert 条件做模糊查找,会直接失败
upsert 时如何避免覆盖已有字段
默认 $set 只更新指定字段,但如果你用了 $setOnInsert,就能控制“仅首次插入时才设默认值”。这点容易被忽略,导致二次 upsert 把原本已有的 createdAt 覆盖成新时间。
典型场景:同步订单状态,要保留首次创建时间,但每次更新最新状态和更新时间。
[
{
updateOne: {
filter: { orderId: "ORD-123" },
update: {
$set: { status: "shipped", updatedAt: new Date() },
$setOnInsert: { createdAt: new Date() }
},
upsert: true
}
}
]
-
$set每次都生效,适合状态、时间戳等需持续更新的字段 -
$setOnInsert仅在真正插入新文档时触发,适合createdAt、source、version初始值 - 别混用
$set和replaceOne——后者会删掉所有未提及字段,upsert 时尤其危险
bulkWrite upsert 的性能和顺序问题
bulkWrite 默认按顺序执行,但 upsert 操作之间**不保证原子性交叉安全**。如果两个并发请求同时对同一 filter upsert,可能触发两次插入(违反唯一索引)或其中一次转为更新——这取决于 MongoDB 内部锁粒度和索引是否存在。
所以,必须提前建好唯一索引,否则 upsert: true 在高并发下大概率抛 duplicate key 错误。
- 确保
filter中所有字段都有对应唯一复合索引,例如db.orders.createIndex({ orderId: 1 }, { unique: true }) - bulkWrite 最多支持 1000 个操作,超量要分批;每批内操作越多,单次网络往返收益越高,但失败回滚成本也越高
- 如果批量中混有 insert/update/upsert,注意失败操作不会中断后续执行(除非加
ordered: false),但 upsert 失败通常意味着数据逻辑冲突,建议保留ordered: true默认值
错误信息里看到 “E11000 duplicate key” 怎么办
这不是代码写错了,是 MongoDB 在尝试插入时撞上了唯一索引约束——说明你的 filter 没覆盖到实际重复依据,或者索引没建对。
先检查三条线:索引定义、bulkWrite 中的 filter 内容、真实集合里已存在的文档结构。
- 用
db.orders.getIndexes()确认唯一索引字段名和顺序,和filter键名严格一致(包括大小写) - 手动查一条疑似冲突的数据:
db.orders.findOne({ orderId: "ORD-123" }),看它是否真存在,且字段值是否和你传的完全匹配(字符串空格、null vs undefined 都算不同) - 如果索引是复合的(如
{ a: 1, b: 1 }),filter也必须同时含a和b,少一个就无法命中唯一约束,导致插入而非更新
最常被忽略的是:开发环境建了索引,上线后忘记在生产库运行相同命令。











