在mongoose中需用index({field1: 1, field2: 1}, {unique: true})定义复合唯一索引,字段必须显式指定排序值(1或-1),字段级unique: true仅支持单字段索引,无法满足多字段联合唯一业务需求。

在Mongoose Schema中用 index() 定义复合唯一索引
直接在 Schema 构造后调用 index() 是最常用也最稳妥的方式。Mongoose 会把该索引定义同步到 MongoDB,且支持多字段组合 + 唯一约束。
注意:index() 调用本身不触发建索引操作,需配合 ensureIndex()(旧版)或等待连接时自动构建(推荐),或手动执行 db.collection.createIndex()。
常见错误是只写字段名不加排序标识(如 1 或 -1),导致语法报错或索引行为异常:
const userSchema = new Schema({
email: String,
tenantId: String
});
// ✅ 正确:每个字段明确指定升序/降序
userSchema.index({ email: 1, tenantId: 1 }, { unique: true });
// ❌ 错误:缺少排序值,Mongoose 会静默忽略该 index 声明
userSchema.index({ email: 'email', tenantId: 'tenantId' }, { unique: true });
为什么不能只靠 unique: true 在字段级声明
字段级 unique: true(如 { email: { type: String, unique: true } })只能为单字段创建唯一索引,无法表达「email + tenantId 联合唯一」这类业务规则。
典型场景是 SaaS 系统:不同租户(tenantId)下允许相同邮箱,但同一租户内邮箱必须唯一。此时字段级唯一会全局拦截,直接破坏业务逻辑。
另外,字段级唯一索引名由 Mongoose 自动生成(如 email_1),而复合索引名默认为字段名拼接(如 email_1_tenantId_1),后者更易识别和排查。
- 字段级
unique实际是语法糖,底层仍调用index() - 多个字段同时设
unique: true→ 生成多个单字段索引,不是复合索引 - 混合使用(字段级 +
index())会导致冗余索引,增加写入开销
复合唯一索引的常见陷阱与绕过条件
MongoDB 的唯一索引对 null 和缺失字段有特殊处理:只要任意参与字段为 null 或完全不存在,该文档就**不参与唯一校验**。这意味着以下两条文档可同时插入:
{ email: "a@b.com", tenantId: null }
{ email: "a@b.com", tenantId: undefined }
如果你的业务要求「空值也要参与去重」,必须提前清洗数据或改用部分索引(partial index)+ 显式条件过滤,例如:
userSchema.index(
{ email: 1, tenantId: 1 },
{
unique: true,
partialFilterExpression: { email: { $exists: true }, tenantId: { $exists: true } }
}
);
- 确保应用层不写入
null或空字符串作为tenantId,否则索引形同虚设 - 更新操作(
findOneAndUpdate)同样受该索引约束,重复时抛E11000 duplicate key错误 - 索引字段顺序影响查询性能:高频查询条件应放前面(如
tenantId通常比email更常用于筛选)
验证索引是否生效及调试方法
光写 index() 不等于索引已建好。Mongoose 默认在首次连接成功后自动调用 createIndexes()(需启用 autoIndex: true,这是默认值),但开发时容易忽略确认步骤。
最直接的验证方式是在 MongoDB Shell 中运行:
db.users.getIndexes()
检查输出中是否存在类似这样的条目:
{
"v": 2,
"key": { "email": 1, "tenantId": 1 },
"name": "email_1_tenantId_1",
"unique": true
}
- 若没看到,可能是
autoIndex: false被显式关闭,或连接未完成就退出 - 可在连接后手动触发:
await mongoose.connection.db.collection('users').createIndex({ email: 1, tenantId: 1 }, { unique: true }) - 线上环境建议在部署脚本中加索引检查逻辑,避免因索引缺失导致静默数据污染
真正麻烦的不是怎么写,而是字段值为空、索引未真正创建、以及错误被上层吞掉——这三者叠加时,问题往往拖到数据对不上才暴露。











