加 unique: true 仍插入重复数据的主因是索引未生效或字段值存在隐藏差异(如空格、大小写、null 处理);需检查索引状态、清理重复数据、合理配置 collation 或 sparse 选项。

为什么加了 unique: true 还会插入重复数据
常见原因是索引没真正建成功,或者字段值看似相同实则存在隐藏差异。比如字符串末尾有空格、大小写不一致(collation 未设)、null 值被多次插入(MongoDB 认为多个 null 不冲突)。另外,如果集合已有重复文档,建唯一索引会直接失败,必须先清理数据。
实操建议:
- 建索引前用
db.collection.find({ field: { $exists: true } }).distinct("field")检查实际值分布 - 对大小写敏感场景,显式指定
collation: { locale: "en", strength: 2 } - 若字段允许
null,且业务上需视为“唯一占位”,可改用稀疏索引:{ unique: true, sparse: true } - 建索引后务必执行
db.collection.getIndexes()确认索引状态为"ready"
updateOne + upsert: true 怎么避免覆盖已有字段
Upsert 默认是“全量替换”逻辑:如果匹配到文档,就用 $set 或整个文档内容去更新;没匹配到才插入。但很多人误以为它只更新部分字段,结果把未传入的字段清空了。
实操建议:
- 永远用
$set操作符,而不是直接传入文档对象。例如:{ $set: { name: "Alice", email: "a@b.com" } },而非{ name: "Alice", email: "a@b.com" } - 如果要保留旧值、仅更新非空新值,得配合
$setOnInsert和条件更新,比如:{ $set: { updatedAt: new Date() }, $setOnInsert: { createdAt: new Date() } } - 注意
upsert在分片集群中可能引发跨片路由问题,确保查询条件包含分片键,否则报错Cannot upsert with query that does not contain the shard key
UniqueIndex 和 Upsert 选哪个更合适
不是二选一,而是分工不同:唯一索引是兜底校验层,防止脏数据入库;Upsert 是应用层的写入策略,用于幂等更新。两者常一起用——先靠索引拦住非法重复,再靠 Upsert 处理常规业务更新。
实操建议:
- 主键或业务强唯一字段(如
email、phone)必须建unique索引,这是数据一致性的最后一道防线 - Upsert 更适合“以某字段为标识做状态同步”的场景,比如用户资料刷新、设备心跳上报
- 高并发写入下,仅靠 Upsert 可能因竞态导致重复插入(两个请求同时没查到,然后都插入),此时必须依赖唯一索引抛出
11000 duplicate key错误来重试 - 索引有写开销,别给所有字段都加
unique;Upsert 本身无额外开销,但逻辑复杂度会上升
遇到 duplicate key 错误该怎么处理
MongoDB 抛出的 E11000 duplicate key(驱动里常表现为 11000 或 11001 错误码)不是异常,而是明确的业务信号:你试图插入一个已被唯一约束拦截的文档。
实操建议:
- 不要捕获后静默吞掉,而应根据场景决定是忽略、更新还是告警。例如注册时邮箱重复,就返回“邮箱已存在”
- Node.js 驱动中检查错误:
if (error?.code === 11000);Python PyMongo 中用error.code == 11000 - 如果想自动转为更新,可在捕获后立刻执行一次
updateOne,但要注意这会多一次网络往返,不如一开始用 Upsert - 日志中记录完整
error.errmsg,里面含具体冲突字段和值,比如:"E11000 duplicate key error collection: test.users index: email_1 dup key: { email: \"a@b.com\" }"
真正容易被忽略的是索引构建的异步性——在副本集上,索引创建完成不代表所有节点立即生效;还有 partialFilterExpression 配合唯一索引时,规则边界稍不注意就会漏掉校验。这些细节不报错,但会让“防重”变成幻觉。


















