upsert不能替代唯一索引,它仅提供“查不到就插、查到了就更新”的原子操作,真正防止重复插入的是唯一索引;高并发下依赖upsert易触发E11000错误,唯一索引才是数据一致性的强制护栏。

upsert 不能替代唯一索引,但能规避部分重复插入场景
直接说结论:upsert 本身不防止重复插入,它只是“查不到就插、查到了就更新”的原子操作。如果你依赖它来避免 E11000 duplicate key error,那大概率会在高并发下翻车——两个请求同时 findOne 没查到,然后都执行 insertOne,最终撞上唯一索引报错。
真正起兜底作用的是唯一索引(unique: true),upsert 只适合做业务逻辑层面的“状态同步”,比如刷新用户资料、上报设备心跳。
-
upsert对性能无额外开销,但要求你明确指定查询条件(如{ email: "a@b.com" }),且该字段必须有索引(否则慢) - 如果查询条件字段没建索引,MongoDB 会全表扫描,
upsert变成性能黑洞 -
upsert: true配合updateOne时,更新动作只影响匹配到的文档;若没匹配到,才插入新文档——它不保证插入数据的字段组合全局唯一
为什么 updateOne + upsert:true 仍可能抛 E11000 错误
错误常发生在你用 updateOne 更新一个带唯一约束的字段,而新值与其他已有文档冲突。例如:
db.users.updateOne(
{ _id: ObjectId("...") },
{ $set: { username: "admin" } },
{ upsert: true }
)
假设集合中已存在 { username: "admin" } 的文档(哪怕 _id 不同),这个操作就会触发 E11000:因为 username 字段有唯一索引,而你试图让两条文档共享同一个 username。
- 这不是
upsert的 bug,是唯一索引在尽责拦截非法状态 - 解决路径只有两种:提前校验目标值是否已被占用;或改用
replaceOne(但需传入完整文档,且仍受唯一索引约束) - 别在
$set中更新任何带unique索引的字段,除非你确认该值未被其他文档使用
批量 upsert 的坑:ordered=false 也不保险
用 bulkWrite 批量 upsert 时,即使设了 ordered: false,只要某条操作违反唯一索引,MongoDB 仍会立即终止该条操作并抛 E11000,其余操作照常执行——听起来安全?其实不然。
问题在于:批量中的多条 upsert 可能命中同一查询条件(比如都查 { h_id: "xxx" }),而 MongoDB 不保证它们的执行顺序。结果就是:其中一条成功插入,另一条因查到已存在而执行 update,但如果 update 内容又触发另一个唯一字段冲突(比如 date 字段也参与复合唯一索引),照样报错。
- 复合唯一索引(如
{ h_id: 1, date: 1 })会让批量 upsert 的冲突判断变复杂,必须确保每条数据的组合键真正唯一 - 不要指望
ordered: false能“自动跳过”所有冲突;它只跳过单条失败,不解决语义冲突 - 真正稳妥的做法是:先用
distinct或countDocuments预检关键字段值是否已存在,再决定走 insert 还是 update
事务内捕获 DuplicateKeyError 是无效的
在 MongoDB 事务里写类似这样的代码:
await session.withTransaction(async () => {
try {
await collection.insertOne({ _id: "abc", name: "test" });
} catch (err) {
if (err.code === 11000) {
await collection.updateOne({ _id: "abc" }, { $set: { name: "test" } });
}
}
});
它不会按预期工作。一旦 insertOne 报 E11000,事务立刻被服务端标记为 aborted,后续任何写操作(包括那个 updateOne)都会立刻抛 InvalidTransactionOperationError。
- 事务内无法“降级处理”唯一键冲突,MongoDB 不允许 abort 后继续写
- 可行方案只有两个:事务外提前
findOne查重;或改用非事务逻辑,靠唯一索引 + 重试机制兜底 - 别在事务回调里 try/catch 插入再补 update——那行代码根本执行不到
最常被忽略的一点:唯一索引不是性能装饰品,它是数据一致性的强制护栏。删掉它来“解决” E11000,等于拆掉刹车去跑山路。











