$lookup在分片集群中特别慢,是因为foreignfield必须手动建单字段b-tree索引且每个分片均需存在,否则触发全分片扫描;若分片键与foreignfield不重合,则无法路由而广播查询。

$lookup 在分片集群中能用,但默认不走索引——必须手动为 foreignField 单独建 B-tree 索引,且每个分片上都要存在。
为什么分片环境下 $lookup 特别慢
分片集群中,$lookup 不像单机那样自动利用 foreignField 上的复合索引前缀;它会在每个目标分片上独立执行一次查询,而如果 foreignField 没有单字段索引,就会触发全分片扫描。哪怕数据只分布在 3 个分片,每片 10 万文档,没索引时就是 3 × 10 万次扫描。
- 常见误判:看到
explain输出里有IXSCAN就以为索引生效了——其实那只是 pipeline 前面阶段(比如$match)用的索引,跟$lookup无关 - 真正要查的是
from集合在各分片上的执行计划,得分别连到每个分片(不是 mongos)执行:db.getSiblingDB("your_db").products.explain("executionStats").aggregate([...]) - 分片键和
foreignField不重合时,$lookup无法路由到特定分片,必然广播查询
foreignField 索引必须满足的四个硬性条件
缺一不可,否则索引对 $lookup 完全无效:
-
foreignField字段必须单独建索引,例如:db.products.createIndex({ _id: 1 })或db.products.createIndex({ "user_id": 1 })—— 复合索引如{ status: 1, user_id: 1 }无效 - 嵌套字段必须写完整路径:
db.products.createIndex({ "profile.contact.email": 1 }),不能只写"email" - 该索引必须在每个分片上显式创建,不能只在 mongos 或 config server 上建;可用
sh.status()查分片列表,再逐个sh.enableSharding("db.products")后确认 - 索引类型必须统一:所有分片都得是
B-tree(默认),不能一个分片是哈希索引、另一个是 TTL 索引
localField 索引不是可选项,而是前置加速器
localField 所在集合的索引不直接加速 $lookup,但它决定有多少文档会进入 $lookup 阶段。一旦前面的 $match 没走索引,几万文档涌进来,$lookup 就得重复执行几万次——哪怕每次只查 1ms,总耗时也秒级起步。
- 错误顺序:
[{ $project: { uid: "$user_id" } }, { $lookup: { from: "users", localField: "uid", ... } }]——$project后字段无法被索引命中 - 正确顺序:
[{ $match: { status: "active" } }, { $lookup: { from: "users", localField: "user_id", ... } }],且status字段必须有索引 - 如果
localField是数组(如tags: ["a", "b"]),$lookup会为每个元素各执行一次查询,此时foreignField索引压力翻倍,务必评估是否真需要数组关联
跨分片 $lookup 的两个隐藏限制
MongoDB 6.0 支持分片集合作为 from,但不等于所有组合都可行:
- 如果
from集合是分片的,而 pipeline 中用了$group或$sort且数据量超内存,默认合并操作只能在主分片上执行——这会让整个聚合卡在单点,成为瓶颈 - 跨库
$lookup(如from: "other_db.collection")在分片集群中不支持;mongos 无法协调跨库路由,会直接报错NamespaceNotFound - 使用
pipeline选项替代from+localField/foreignField时,注意$documents阶段不支持分片环境,只能用于单机或副本集
最容易被忽略的一点:索引建完后,必须验证它是否真的被 $lookup 使用——不是看 mongos 的 explain,而是登录每个分片节点,用 db.collection.explain("executionStats").aggregate(...) 查 executionStages 里是否有针对 foreignField 的 IXSCAN,且 nReturned 接近预期匹配数,而不是全量。











