应优先使用 upsert: true 而非先查后插,因后者存在竞态条件、性能损耗、错误分支多及缺乏原子性等问题;upsert 需配合唯一索引,并合理使用 $set 与 $setOnInsert,必要时用 findOneAndUpdate() 获取新文档。

直接用 count_documents() 或 find_one() 判断存在性,但绝大多数场景下——你应该跳过判断,直接用 upsert: true。
为什么不用先查再插?
看似清晰的“先 count_documents() 再 insert_one()”流程,实际埋着竞态漏洞:两个并发请求同时查到“不存在”,接着都执行插入,结果重复入库。这不是小概率事件,高并发或网络延迟时极易触发。
更麻烦的是,这种写法多一次 round-trip,性能损耗明显,且代码变长、错误分支增多(比如你要分别处理连接失败、权限拒绝、查询超时等)。
-
count_documents()只返回数字,不带索引提示,若没建对应索引,大数据集上会全表扫描 -
find_one()虽然能提前终止,但依然要序列化一个完整文档(哪怕只用_id字段),内存和网络开销比count_documents()更高 - 两者都不保证原子性,无法替代
upsert
upsert 怎么写才不踩坑?
upsert 不是独立命令,而是 updateOne()、updateMany() 和 findOneAndUpdate() 的选项。用错函数或参数,效果就偏了。
- 别用
replaceOne():它默认不 upsert,即使加{ upsert: true },也会把整条文档替换成你传的字段,容易丢数据 - 过滤条件必须有唯一约束:比如用
{ email: "a@b.com" }upsert,就得先确保email字段有唯一索引,否则并发下仍可能插出多条 - 区分更新字段和仅插入字段:用
$set控制每次都要更新的字段(如updatedAt),用$setOnInsert控制只在新建时生效的字段(如createdAt、version)
正确示例:
db.users.updateOne(
{ email: "user@example.com" },
{
$set: { status: "active", updatedAt: new Date() },
$setOnInsert: { createdAt: new Date(), version: 1 }
},
{ upsert: true }
)
需要立刻拿到新文档怎么办?
updateOne() 返回结果里只有 upsertedId(仅当插入发生时才有),没有文档内容。如果你必须马上读取刚写入或更新后的完整数据,得换用 findOneAndUpdate() 并设 returnDocument: "after":
db.users.findOneAndUpdate(
{ email: "user@example.com" },
{
$set: { status: "active", updatedAt: new Date() },
$setOnInsert: { createdAt: new Date(), version: 1 }
},
{ upsert: true, returnDocument: "after" }
)
注意 Node.js 驱动里参数名是 returnDocument,不是已废弃的 returnNewDocument。
真正难的从来不是语法,而是想清楚:你到底要“避免重复”还是“必须知道是否存在”。前者用 upsert,后者才真需要查——但查完也别手写 insert,该加唯一索引就加,该用事务就用事务。

















