软删除字段必须统一为布尔型 isdeleted,复合唯一索引应为 {field: 1, isdeleted: 1} 或使用 partialfilterexpression: {isdeleted: false} 的部分索引,并确保存量数据补全 isdeleted 字段后再建索引。

软删除字段名和类型必须统一为 isDeleted 布尔型
用 deleted、soft_deleted 或字符串枚举(如 "deleted")都会破坏唯一性约束的可靠性。MongoDB 的唯一索引对 null、缺失字段、false 视为不同值,一旦字段类型不一致(比如部分文档是 isDeleted: "true",部分是 isDeleted: true),复合索引就无法稳定生效。更麻烦的是,字符串值大小写或空格("Deleted"、" deleted")会让 $eq 查询漏匹配,而布尔型只有 true/false 两种确定状态。
唯一性校验必须用复合索引 { field: 1, isDeleted: 1 }
只在业务字段(如 email)上建唯一索引是错的——它允许插入多个 isDeleted: true 的同值文档,后续恢复时可能冲突。正确做法是显式建复合唯一索引:
db.users.createIndex({ email: 1, isDeleted: 1 }, { unique: true })
这样,{ email: "a@b.c", isDeleted: false } 和 { email: "a@b.c", isDeleted: true } 被视为两个独立键,可共存;但不允许出现两个 isDeleted: false 的相同邮箱。注意:插入新文档时必须明确传 isDeleted: false,不能依赖默认值,否则字段缺失会导致索引条目为 { email: "a@b.c", isDeleted: null },和 false 不冲突。
Partial index 更省空间,但查询必须带 isDeleted: false
如果软删文档占比高,用 partial index 替代复合唯一索引能减少索引体积和写入开销:
db.users.createIndex({ email: 1 }, { unique: true, partialFilterExpression: { isDeleted: false } })
这个索引只覆盖 isDeleted: false 的文档,但代价是:所有依赖该索引的查询(如查邮箱是否存在)都必须显式带上 isDeleted: false 条件,否则 MongoDB 不会命中该索引。常见错误是写 db.users.findOne({ email: "a@b.c" }),结果走全表扫描。DAO 层必须强制封装,禁止裸查。
已有集合加软删除字段时,存量数据要批量补全再建索引
直接在老集合上加 isDeleted 字段,不处理历史数据,会导致索引中混入缺失/空值,让唯一性逻辑失效。分三步操作才稳妥:
- 先建稀疏索引:
db.collection.createIndex({ isDeleted: 1 }, { sparse: true }),避免建索引时卡住 - 再批量更新缺失字段:
db.collection.updateMany({ isDeleted: { $exists: false } }, { $set: { isDeleted: false } }),大集合需按_id分批执行 - 最后删掉稀疏索引,重建非稀疏索引或直接建复合唯一索引
这一步最容易被跳过,尤其在灰度上线时——只要有一条文档没补 isDeleted,后续任何基于该字段的唯一约束都可能在某次插入时突然报错。











