unique: true 仅声明索引而非运行时校验,需手动调用 syncindexes() 或启用自动创建,并验证数据库中索引存在且状态为 ready,同时处理重复数据、null 值、隐藏字符、大小写及分片集群下片键前缀等限制。

unique: true 在 Mongoose 中只是声明索引,不是运行时校验器;它不自动创建、也不保证生效——你得确认索引真在数据库里,且结构合规。
为什么 unique: true 注解看起来没用
常见错觉:加了注解就万事大吉。实际是 Mongoose 默认不主动建索引,只在你显式调用 Model.syncIndexes() 或开启自动创建时才落地。
更隐蔽的问题是:已有重复数据、字段含空格或 null、分片集群下没带片键前缀,都会让索引“存在但无效”。
典型现象:E11000 duplicate key error 从不抛出,或者只在某些插入路径触发。
检查并修复索引是否真被创建
别信代码注解,直接查数据库:
- 连接 MongoDB Shell,运行
db.users.getIndexes(),确认输出里有"key": {"email": 1}, "unique": true且"state": "ready" - 如果没看到,说明索引根本没建——Mongoose 默认不自动创建,除非你配置了
spring.data.mongodb.auto-index-creation=true(Spring)或手动调用了User.syncIndexes()(Node.js) - 如果看到但状态不是
ready,可能是建索引中途失败,比如集合里已存在重复email值,MongoDB 拒绝建索引并静默失败 - 查重复值:
db.users.aggregate([{$group: {_id: "$email", count: {$sum: 1}}}, {$match: {count: {$gt: 1}}}, {$project: {_id: 0, email: "$_id", count: 1}}])
处理 null 和隐藏字符导致的“假重复”
MongoDB 把多个 null 当作互不冲突,所以 {email: null} 插入多条不会报错。这不是 bug,是默认行为。
- 业务上不允许空邮箱?那就别留
null,改用required: true+ 应用层拦截 - 必须允许空值?放弃
unique: true,改用稀疏唯一索引:unique: true, sparse: true—— 它会跳过所有email缺失或为null的文档,只对非空值强制唯一 - 字段值看似相同却能重复插入?检查不可见字符:
db.users.findOne({email: /alice/}).email.charCodeAt(0)看首字符编码,排查全角空格、\u200b零宽字符等 - 大小写不敏感去重?加
collation: {locale: "en", strength: 2},否则"Alice"和"alice"被视为不同
分片集群下唯一索引必须带片键前缀
如果你用的是分片集群,createIndex({email: 1}, {unique: true}) 即使成功,也只在单个分片内生效——跨分片重复完全不受控。
- 先确认片键:
sh.status(),比如片键是{orgId: 1} - 唯一索引必须以片键开头:
db.users.createIndex({"orgId": 1, "email": 1}, {unique: true}),不能是{"email": 1}或{"createdAt": -1, "email": 1} - 复合索引中片键字段必须严格最左,不能跳过;哪怕片键是
{orgId: 1, userId: 1},你也得写成{"orgId": 1, "userId": 1, "email": 1} - 应用层仍需兜底:块迁移、写关注设为
w: 1时主节点宕机,都可能短暂突破唯一性——索引不是银弹
getIndexes() 输出、清掉历史脏数据、再测并发插入。漏掉任何一环,unique 就只是个摆设。











