MongoDB不支持复合主键,_id必须是单值(可为嵌套文档但不可部分更新),真正保障字段组合唯一性的是唯一复合索引,如db.orders.createIndex({user_id:1,tenant_id:1},{unique:true}),插入重复组合时触发E11000错误。

MongoDB 本身不支持传统意义上的“复合主键”,_id 字段必须是单个值(不能是数组),但可以通过文档结构 + 唯一复合索引模拟等效行为。
为什么不能直接用 {a: 1, b: 2} 当 _id
你写 db.collection.insertOne({ _id: { a: 1, b: 2 }, ... }) 是合法的——MongoDB 允许 _id 是嵌套文档。但问题在于:
• 它仍是**一个字段值**,不是两个独立主键;
• 该嵌套文档一旦写入,就不能部分更新 _id(整个 _id 不可修改);
• 更关键的是:MongoDB **不会自动保证该嵌套结构内各字段的唯一性组合**,除非你额外建唯一索引。
真正起作用的是唯一复合索引,不是 _id 结构
所谓“复合主键效果”,实际靠的是:
• 在业务字段(如 user_id 和 tenant_id)上创建唯一复合索引;
• 插入时依赖索引报错拦截重复;
• 查询时用这些字段做高效定位。
示例:
db.orders.createIndex({ user_id: 1, tenant_id: 1 }, { unique: true })
之后执行:
db.orders.insertOne({ user_id: 1001, tenant_id: "prod", amount: 99.9 })
若再插入相同 user_id + tenant_id 组合,会触发错误:MongoServerError: E11000 duplicate key error
要点:
- 索引字段顺序影响查询效率:高频过滤字段放前面(如
tenant_id通常比user_id更常用于租户隔离) - 不能依赖
_id的 ObjectId 时间戳来排序分页——它只在单机生成时近似有序,分布式环境下不可靠 - 如果业务真需要“逻辑主键”显式可见,可同时保留
_id(自动生成)和复合业务字段,并确保二者约束一致
嵌套 _id 文档的坑:序列化与比较陷阱
虽然 _id: { a: 1, b: "x" } 能存,但要注意:
• 不同驱动对嵌套 _id 的序列化行为可能不一致(比如 Python PyMongo 和 Node.js MongoDB Driver 对字段顺序敏感);
• 查询时 { _id: { a: 1, b: "x" } } 必须字段名、顺序、类型完全匹配,否则查不到;
• $gt/$lt 等范围操作对嵌套文档无效,无法用于游标分页。
所以,除非有强需求(例如迁移旧系统需保持 ID 格式),否则更推荐:
• 用默认 ObjectId 作物理主键;
• 用业务字段 + 唯一复合索引实现逻辑唯一约束;
• 分页走 created_at 或 seq_id 字段,而非 _id。
设计阶段就要决定:哪些字段组合必须唯一?
这比纠结“能不能当主键”更重要。常见场景:
- 多租户 SaaS:
{ tenant_id: 1, email: 1 }唯一索引,防止同一租户下邮箱重复 - 设备上报数据:
{ device_sn: 1, timestamp: 1 }复合索引,配合 TTL 自动过期 - 订单子项:用
order_id+line_no构成业务主键,建唯一索引并用于快速定位明细
记住:MongoDB 的“主键思维”要转向「查询路径 + 约束保障」。索引不是锦上添花,而是模型设计的一部分——漏掉唯一索引,等于没设主键。


















