绝大多数真实业务场景下,不要用双向数组引用,优先选单向引用+$lookup聚合,关系带属性时必须拆中间集合;因双向数组存在双写风险、删除低效、查询无法索引、易超文档大小限制等问题,而$lookup更可靠且可控。

直接说结论:绝大多数真实业务场景下,不要用双向数组引用,优先选单向引用 + $lookup 聚合,关系带属性(如时间、角色)时必须拆中间集合。
为什么$lookup比双向数组更可靠
很多人以为在 users.groups 和 groups.users 里互相存 ID 能省一次查询,但实际代价远超收益:
- 写入时要双写,
$addToSet并发不保险,$push容易塞重复 ID - 删一个
user,得用bulkWrite扫所有groups文档做$pull,N+1 查询会打崩数据库 - 查“用户 A 最近加入的 10 个群组”这种需求,数组字段无法对嵌套时间戳建有效索引,也做不了
$gt查询 - 数组膨胀后,单文档超 16MB 限制或触发
Exceeded memory limit for $group(平均每个 user 关联超 500 组就大概率出事)
单向引用 + $lookup 的实操要点
比如 users 集合里只存 group_ids: [ObjectId],查用户及所属群组名时走聚合:
- 必须前置
$match,例如{$match: {status: "active"}},否则主集合全量扫描再逐条 join -
groups集合的_id字段必须建索引,否则每次子查询都是全表扫 -
$lookup后紧跟$project,例如{$project: {"groups.name": 1, "groups._id": 1}},避免拖回整个 group 文档 - 如果关联数常超 500,考虑加
$limit或改用中间集合
什么时候必须用中间集合(如 user_groups)
只要关系本身携带业务字段——比如 joined_at、role、is_muted,数组引用立刻失效:
- 中间集合结构应为:
{user_id: ObjectId, group_id: ObjectId, joined_at: Date, role: String} - 必须建复合索引:
db.user_groups.createIndex({user_id: 1, joined_at: -1}),支撑“某用户最近加入的群组” - 高吞吐场景下,
user_id或group_id可设为分片键,避免写热点 - 查“用户 A 在哪些群有管理员权限”,直接
db.user_groups.find({user_id: ..., role: "admin"}),干净利落
真正容易被忽略的是:中间集合不是“过度设计”,而是把关系语义显式落地。当你开始纠结“该不该加 joined_at 字段”时,答案已经很明确了。











