$lookup在分片集群中变慢的根本原因是MongoDB无法路由到单一分片,被迫广播至所有分片执行再合并结果;仅当let+pipeline且变量来自分片键前缀、$match严格对等时才可本地路由。

为什么$lookup在分片集群里一用就慢
根本原因不是语法写错,而是 MongoDB 无法把 $lookup 路由到单一分片——它被迫广播给所有分片执行,再由 mongos 合并结果。一次查询变成 N 次远程调用,网络往返、序列化、内存缓冲全堆在一起,几十毫秒变几秒是常态。
常见错误现象包括:
- 查询不报错,但响应时间从
20ms跳到3200ms,且波动极大 -
explain("executionStats")显示shards数量等于集群分片总数(比如 6 个),且每个分片的nReturned远高于预期 - mongos 日志里高频出现
scatterGather或remote execution
localField/foreignField 模式在分片下基本失效
这种写法看着简洁,但在分片环境里几乎等同于主动触发广播:
{ $lookup: { from: "orders", localField: "userId", foreignField: "_id", as: "orders" } }
只要 orders 集合没按 userId 分片,或者主集合的 userId 不是其分片键前缀,MongoDB 就无法判断该去哪个分片查,只能全发。
真正能走本地路由的,只有 let + pipeline 形式,且满足两个硬性条件:
-
let中绑定的变量,必须来自主集合的分片键或其前缀字段(如分片键是{tenantId: 1, orderId: 1},那let: {tid: "$tenantId"}安全,let: {uid: "$userId"}不安全) -
pipeline中的$match必须严格使用$$变量与foreignField对等比较,不能掺杂表达式(如$toString)
怎么验证你的 $lookup 真正在本地执行
别只看结果对不对,重点看执行路径是否被优化:
- 执行
db.collection.aggregate([...]).explain("executionStats"),检查shards字段:如果只列出 1 个分片,说明路由成功;若列出全部,就是广播 - 对比
totalDocsExamined和你预估的匹配文档数:如果前者是后者的 5 倍以上,大概率发生了跨片低效扫描 - 在 mongos 日志中 grep
"orders"(替换成你的from集合名),确认是否只出现在单一分片日志行里
注意:即使两个集合都按 tenantId 分片,若你在 pipeline 里写了 {$match: {$expr: {$eq: ["$status", "paid"]}}},而 status 不是分片键字段,照样会跨片——MongoDB 只认分片键字段的值来做路由决策。
绕不开时,应用层双查比硬扛 $lookup 更稳
当业务强依赖关联,又无法统一分片键,别死磕 $lookup。应用层双查虽然多一次 round-trip,但能确保每次查询都走分片路由,避免广播放大延迟:
- 第一步:查主集合,拿到关键 ID 列表(如
userIds = ["u1", "u2", ...]) - 第二步:用
find({ _id: { $in: userIds } })查副集合——只要副集合按_id分片,这次查询就只会打到对应分片 - 最后在应用内存里做关联合并,可控、可缓存、可加超时熔断
这个做法的隐含代价是:你要自己处理空值、重复、顺序一致性等问题。但比起跨片 $lookup 导致的随机超时和 OOM,这点复杂度值得付。



















