$lookup 的性能完全取决于外键有没有索引?这篇用 20 万订单关联 2 万客户,看 explain 的 strategy,对比耗时随输入量的增长,再看 let / pipeline 形式和结果大小限制。
test> $lookup {localField: "cust", foreignField: "_id"} // foreign 有 _id 索引
{
strategy: 'IndexedLoopJoin',
foreignCollection: 'test.customers',
foreignIndex: '_id_'
}
test> $lookup {localField: "ccid", foreignField: "cid"} // foreign 没有索引
{
strategy: 'NestedLoopJoin',
foreignCollection: 'test.customers',
foreignIndex: null
}explain 里的 EQ_LOOKUP 阶段带 strategy:外键有索引时是 IndexedLoopJoin,没有索引是 NestedLoopJoin。
100 条订单:有索引(cust → _id) 0.7 ms;没有索引(ccid → cid) 247 ms 1000 条订单:有索引(cust → _id) 3.9 ms;没有索引(ccid → cid) 2367 ms
1,000 条订单关联 2 万个客户:有索引 3.9 ms,没有索引 2,367 ms,慢约 600 倍,因为每条订单都要扫一遍客户。
test> $lookup {localField: "ccid", foreignField: "cid"} // 建了 cid 索引
{
strategy: 'IndexedLoopJoin',
foreignCollection: 'test.customers',
foreignIndex: 'cid_1'
}
100 条订单:有索引(ccid → cid) 0.6 ms
1000 条订单:有索引(ccid → cid) 4.3 ms给外键 cid 建索引,同样的 $lookup 立刻变成 IndexedLoopJoin,耗时回到 4.3 ms。
订单数 有索引 无索引
100 0.6 ms 247 ms
1,000 4.0 ms 2389 ms
10,000 35 ms 24016 ms两种情形都随输入线性增长,斜率相差约 690 倍:10,000 条订单,有索引 35 ms,没有索引 24 秒。
test> let / pipeline 形式({$match: {$expr: {$eq: ["$_id", "$$c"]}}})
{ strategy: null, foreignCollection: null, foreignIndex: null }
1000 条订单:let/pipeline 形式 22 ms;普通 localField/foreignField 形式 4.2 ms子管道形式可以写任意条件,但 explain 里没有等值连接的 strategy,1,000 条订单 22 ms,慢约 5 倍。
test> $lookup 匹配 20 个 1 MiB 的文档,看结果文档有多大
{ _id: 1, n: 20, bytes: 20972239 }
test> 同样的结果 $out 写进集合
Executor error during aggregate command on namespace: test.one :: caused by :: Serializing Document failed :: caused by :: Size 20972239 exceeds maximum 16793600
test> 先 $unwind 展开再 $project 去掉大字段
[ { n: 20 } ]$lookup 的匹配结果是一个数组字段,整条结果文档可超过 16 MiB 返回客户端,但写进集合($out)会失败。