bigywater.tech
MongoDB · 49

$lookup:有无索引的执行差别

$lookup 的性能完全取决于外键有没有索引?这篇用 20 万订单关联 2 万客户,看 explain 的 strategy,对比耗时随输入量的增长,再看 let / pipeline 形式和结果大小限制。

3.9 对 2,367 ms
1,000 条订单 $lookup:外键有索引与没有索引
24,016 ms
没有索引时 10,000 条订单 $lookup 的耗时(有索引 35 ms)
IndexedLoopJoin
外键有索引时 EQ_LOOKUP 的执行策略(没有索引是 NestedLoopJoin)

strategy

实测 · 本机执行输出
  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 秒。

let / pipeline

实测 · 本机执行输出
  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)会失败。

分步演示

$lookup 与索引
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。20 万个订单、2 万个客户;耗时为 pymongo 的 3~5 次中位数(没有索引的 10,000 条只跑 1 次)
    • 没有验证:HashJoin 策略出现的条件(foreign 集合很小且没有索引时,这里 2 万个客户仍是 NestedLoopJoin);$lookup 后接 $unwind 的合并优化;分片集群上的 $lookup 限制;$graphLookup;经典引擎(非 SBE)下的 $lookup 执行方式;一对多关联的结果规模对耗时的影响
    京ICP备2026067502号-1京公网安备11010502063487号