嵌入适用于一对少、数量稳定且增长缓慢的场景,如用户地址(≤20条);若子文档数量波动大、需条件查询或独立生命周期,则必须引用并建索引。

没有“更合适”,只有“当前读写模式下不崩”。嵌入和引用不是设计偏好,而是对数据增长、查询方式、更新频率的现实妥协。
子文档数量是否稳定且可控?看 addresses 而不是 orders
嵌入只适用于一对少、数量明确、增长缓慢的场景。比如用户收货地址,平均 3–5 条,历史最大值不超过 20;文章标签通常 ≤10 个;设备配置项基本固定。
- 用
db.users.findOne().addresses.length快速抽样,若中位数 >20 或方差极大(有的用户 2 条,有的 180 条),别嵌入 - 哪怕现在只有 5 条,查下过去 3 个月月均新增量:如果
addresses每月涨 10%,半年后就可能超限 - 含二进制字段(如
thumbnail、transcript)的子文档,即使数量少也建议引用——单文档体积容易逼近 16MB 上限
是否总要“父+全部子”一起查?警惕 $elemMatch 和 arrayFilters
嵌入省查询的前提是:你每次都要所有子数据。一旦开始条件筛选或分页,性能拐点立刻出现。
-
db.users.find({ "orders.status": "pending" })会全量扫描每个用户的orders数组,无法跳过不匹配项 - 想按
orders.createdAt倒序取前 5 条?嵌入方案得把全部订单拉到内存再排序;引用方案可直接db.orders.find({ user_id: ObjectId("") }).sort({ createdAt: -1 }).limit(5)走索引 - 聚合时用
$unwind拆嵌入数组,数据量一过万,CPU 和内存开销陡增,远不如原生集合查询
子文档是否有独立生命周期?检查 ObjectId 类型与索引
如果子数据要单独更新、审计、被其他服务消费,或者需参与多文档事务,它就不再是“附属”,而是实体。
- 引用字段必须存为原生
ObjectId,不能存字符串"507f1f77bcf86cd799439011"——否则db.orders.createIndex({ user_id: 1 })创建的索引无效 - 务必在引用字段上建索引:
db.orders.createIndex({ user_id: 1 }),否则find({ user_id: ... })就是全表扫描 - 需要按状态过滤?建复合索引:
db.orders.createIndex({ user_id: 1, status: 1 });索引最好在第一批数据写入前就建好
业务演进是否已暴露嵌入瓶颈?
模型不是上线就定死的。当实际流量或数据形态变化时,嵌入可能突然变成枷锁。
- 发现
addresses字段里开始出现几百条历史记录,或业务方提出“查所有深圳地址”需求——嵌入已失效,必须切引用 - 用户文档里
books: [ObjectId(), ObjectId(), ...]数组长度持续超过 100,$push性能明显下降,说明引用退化为“伪嵌入” - 订单要关联支付、物流、优惠券等多个外部系统,且需跨集合原子更新——只能靠引用 + 多文档事务,嵌入完全无解
真正难的不是选嵌入还是引用,而是判断“现在这个读写压力下,哪种方案不会在下周就出问题”。很多崩溃不是因为选错,而是没在数据开始膨胀前及时切换。











