$lookup在分片集群中默认不推送到分片执行,导致全量数据拉取至mongos;MongoDB 4.4引入交换算子下推机制,但需满足右表未分片(或同shard key)、关联字段为_id或唯一索引、且无pipeline三个条件。

为什么 $lookup 在分片集群里特别慢?
因为默认情况下,$lookup 不会下推到各个分片执行,而是在 mongos 上把左表数据拉回来再做关联——相当于把所有分片的匹配文档全扫一遍传到 mongos,网络和内存压力都很大。MongoDB 4.4 引入了“交换算子下推”(exchange pushdown)机制,但仅对满足特定条件的 $lookup 和 $unwind 生效,并非自动开启。
关键前提有三个:右表必须是未分片集合(或与左表同 shard key),关联字段必须是右表的 _id 或唯一索引字段,且 $lookup 不能带 pipeline(即不能用 pipeline 字段)。一旦用了 pipeline,整个 stage 就退回到 mongos 执行。
- 错误写法:
{ $lookup: { from: "orders", localField: "order_id", foreignField: "_id", as: "order", pipeline: [ { $match: { status: "paid" } } ] } }→ 必然不走下推 - 正确写法(可下推):
{ $lookup: { from: "orders", localField: "order_id", foreignField: "_id", as: "order" } },前提是orders是未分片集合,且_id有唯一索引 - 验证是否下推:开启 profiler(
db.setProfilingLevel(2)),查 slowms 日志里是否有"executionStages.stage": "LOOKUP_SHARDING"
$sort + $skip + $limit 组合为何在分片下容易 OOM?
mongos 默认把 $sort 下推到各分片,但每个分片只返回自己的 top-N 结果;如果 N 很大(比如 $limit: 1000),而分片数是 8,mongos 就要合并 8000 条记录再全局排序——中间结果可能远超内存上限。4.4 的交换算子优化允许把 $sort 后置的 $skip/$limit 下推成“局部裁剪”,但需满足:sort key 必须包含 shard key 前缀,且不能有 $group 或 $facet 等阻断下推的 stage。
- 有效下推示例:
{ $sort: { "region": 1, "created_at": -1 } }+{ $limit: 50 },当region是 shard key 时,各分片可直接返回前 50 条,mongos 合并后取全局 top 50 - 无效场景:
{ $sort: { "amount": -1 } }→ 无法下推,所有分片必须返回全部数据供 mongos 排序 - 实操建议:用
explain("executionStats")查看shards.*.executionStages.stage是否出现"SORT_SHARDING",而不是"SORT"
如何强制触发交换算子下推?绕过自动判断失败的情况
MongoDB 4.4 不提供显式开关,但可通过重写查询结构“提示”优化器启用交换算子。最常用的是把原本在 mongos 层做的过滤,提前塞进 $lookup 的 let+pipeline 中——但这看似矛盾:前面说 pipeline 会禁用下推。其实有个例外:当 pipeline 内只含 $match 且 match 条件能被下推为索引扫描时,4.4 仍可能启用 exchange(需配合 hint 和合理索引)。
- 可行写法:
{ $lookup: { from: "logs", let: { uid: "$user_id" }, pipeline: [ { $match: { $expr: { $eq: [ "$user_id", "$$uid" ] } } } ], as: "user_logs" } },前提是logs集合在user_id上有索引,且logs是未分片集合 - 必须加 hint:
db.orders.explain("executionStats").aggregate([...], { allowDiskUse: true, hint: { "user_id": 1 } }),否则优化器可能忽略索引路径 - 注意:这种写法在 5.0+ 被更稳定的
$lookupwithcollation和分布式 join 支持替代,4.4 下属于“技巧性兜底”
最容易被忽略的性能断点:$unwind 后没跟 $match 过滤空数组
$unwind 在分片下默认不触发交换下推,除非后面紧跟一个能利用 shard key 的 $match。如果 $unwind 产生大量空文档(比如 as: "items" 但某些订单无 items),这些空文档仍会跨网络传输,白白消耗带宽和 CPU。4.4 的优化逻辑是:只有 $unwind + $match(且 match 涉及 items 字段)才能触发“空值裁剪下推”。
- 危险写法:
{ $unwind: "$items" }→ 所有分片把 null/missing 数组元素展开成空文档,全发给 mongos - 安全写法:
{ $unwind: "$items" }+{ $match: { "items.sku": { $exists: true } } }→ 各分片可在 unwind 后立刻丢弃空项,不传输 - 验证方式:对比
explain中shards.*.executionStages.nReturned,优化前可能是 10w,优化后应显著下降
交换算子下推不是银弹,它高度依赖查询结构、索引设计和分片键选择。哪怕只漏掉一个 $match 或建错一个索引,整个管道就退回低效模式——调优时得盯着每个 stage 的实际执行位置,而不是只看逻辑写法。

















