$lookup跨分片必广播至所有分片,因MongoDB无法根据localField/foreignField路由;索引仅在单分片内生效,且必须是foreignField的单字段B-tree索引、各分片类型一致、字段类型严格匹配。

$lookup跨分片时根本不会路由到单一分片
因为MongoDB无法根据localField和foreignField值确定目标分片,只能把查询广播到所有分片——这不是“索引没走”,而是压根没机会让索引参与路由。分片键不匹配时,$lookup的from集合哪怕有完美索引,也得先全量拉取各分片数据,在mongos层合并,索引只在单分片内生效,对跨片广播无约束力。
常见错误场景:
- 主集合按
{tenantId: 1, orderId: 1}分片,$lookup却用userId去关联用户表(用户表也按tenantId分片,但userId不是其分片键前缀) -
from集合虽已分片,但foreignField字段未建索引,导致单分片内也全表扫描,叠加广播更慢 - 用了
let+$expr,但let绑定的变量来源字段(如"$$uid")不在分片键路径上
为什么复合索引或分片键前缀索引对$lookup无效
$lookup做等值匹配时,只依赖foreignField单字段的B-tree有序性,不关心它是否嵌套在复合索引里。哪怕你建了{_id: 1, status: 1},只要$lookup查的是_id,MongoDB也不会复用这个索引——它只认{_id: 1}这种单字段索引。
分片环境下还多一层限制:该单字段索引必须在每个分片上显式创建,且类型一致(比如不能一个分片是哈希索引,另一个是普通B-tree)。验证方式不是看explain里有没有IXSCAN,而是检查executionStats.shards里每个分片的totalDocsExamined是否接近实际匹配数;如果远高于预期,说明索引没生效。
let+$expr不是万能解,分片键对齐才是硬门槛
用let+$expr写$lookup,只是把关联逻辑从隐式等值变成显式表达式,但能否路由仍取决于变量来源是否可被分片器识别为路由键。只有当let绑定的字段(如tid: "$tenantId")是from集合分片键的**严格前缀**时,mongos才能把请求精准发往对应分片。
实操要点:
- 两个集合必须用相同分片键(如都按
{tenantId: 1}分片),且$lookup中所有let变量只能来自该键或其前缀字段 - 避免在
pipeline里用$match过滤非分片键字段(如{$expr: {$eq: ["$status", "$$s"]}}),这会让整个from集合在各分片上执行本地扫描 - 分片键含多个字段时(如
{orgId: 1, userId: 1}),let: {oid: "$orgId"}安全,但let: {uid: "$userId"}不行——除非userId单独也是分片键
别指望allowDiskUse缓解跨片瓶颈
allowDiskUse: true只解决单分片内存溢出问题,对跨分片的网络往返、序列化开销、结果集合并延迟毫无帮助。你看到的“查询变慢但不报错”,大概率是mongos正在等所有分片返回几百MB中间数据,然后在自己内存里做$unwind和$group——这时候加磁盘缓存只会让IO更忙,响应更卡。
真正有效的动作只有三个:
- 把高频关联字段冗余进主集合(比如订单文档里直接存
userName),用应用层保证一致性 - 改用应用层双查:
find()先取ID列表,再用find({_id: {$in: [...]}})查副集合——这样能利用分片路由,避免广播 - 确认
from集合的foreignField在每个分片上都有{field: 1}单字段索引,且类型与localField严格一致(ObjectId不能和字符串混用)
最容易被忽略的是字段类型一致性:一边是ObjectId("..."),另一边存的是"..."字符串,$lookup连匹配都失败,更别说走索引了。

















