$lookup慢几乎总是因foreignField未建正确单字段索引,而非数据量大;必须为from集合的foreignField显式创建单字段索引(如{_id:1}),复合索引无效,嵌套字段需完整路径,分片集群须各分片一致,且localField类型与foreignField严格匹配。

$lookup慢,几乎总是因为foreignField没建对索引,而不是数据量大本身。 即使from集合有千万级文档,只要foreignField字段有正确单字段索引,$lookup就能毫秒级完成;反之,哪怕只有几万文档,没索引也会秒级甚至超时。
foreignField必须单独建索引,复合索引完全无效
很多人在products集合上建了{_id: 1, status: 1},以为查_id能用上——实际不能。MongoDB执行$lookup时只做等值匹配:foreignField == localField,它不会走复合索引的非首字段。
- 正确做法:显式为
foreignField字段建单字段索引,例如db.products.createIndex({ _id: 1 }) - 如果
foreignField是嵌套字段(如"metadata.code"),索引必须完整写路径:db.products.createIndex({ "metadata.code": 1 }) - 分片集群下,该索引必须在每个分片上都存在,且类型一致(不能一个分片是哈希索引,另一个是B-tree)
localField索引不是可选的,而是前置加速条件
localField所在集合的索引不直接加速$lookup扫描右表,但它决定有多少文档会流入$lookup阶段。如果第一步$match没走索引,上万文档全进管道,$lookup就得执行上万次关联查询。
- 错误顺序:
[{$project:{productId:1}}, {$lookup:{...}}]——$project后无法利用productId索引过滤 - 正确顺序:先
$match(带索引字段),再$lookup,最后$project - 如果
localField是数组(如tags: ["a","b"]),MongoDB会对每个元素分别发起一次$lookup,索引压力翻倍,需确认业务是否真需要这种展开
验证索引是否真生效,别信explain里的“IXSCAN”
执行计划里出现IXSCAN不代表索引被用于$lookup——它可能只是左表扫描用了索引,而右表仍全表扫描。
- 用
.explain("executionStats")查$lookup阶段的totalDocsExamined和nReturned:理想情况两者应接近;如果totalDocsExamined远大于nReturned,说明索引未命中或字段类型不匹配 - 检查字段值类型是否一致:如果
localField是ObjectId,但foreignField存的是字符串(如"507f1f77bcf86cd799439011"),索引完全失效 - Robo 3T 或 Atlas Query Insights 中,
$lookup节点下若显示“Collection Scan”而非“Index Scan”,基本可断定索引失效
最容易被忽略的是:即使索引语法、字段名、方向全对,只要foreignField值类型与localField不一致,或者分片集群下索引没同步到所有分片,索引就等于没建。性能问题往往卡在这类细节上,而不是模型或架构层面。


















