使用LookupOperation后MongoDB聚合查询耗时随skip值增大变长的问题
MongoDB聚合查询:Skip耗时随Lookup加入变长的原因分析
问题描述
使用SpringBoot 2.7.12 + MongoDB 3.4,仅用SkipOperation和LimitOperation做聚合查询时,不管skip值多大(比如size固定500),查询速度都差不多。但加入LookupOperation后,查询耗时会随skip值增大明显变长:size=500时,skip=0耗时约10ms,skip=10约100ms,skip=100约700ms。
仅Skip+Limit的实现代码
class Repository { public List<EntityResponse> findListOfEntities(int skip, int size) { SkipOperation skipOperation = Aggregation.skip(skip); LimitOperation limitOperation = Aggregation.limit(size); return mongoTemplate.aggregate(Aggregation.newAggregation( skipOperation, limitOperation ),"entity", EntityResponse.class).getMappedResults(); } }
加入Lookup后的实现代码
public List<EntityResponse> findListOfEntities(int skip, int size) { LookupOperation lookupOperation = LookupOperation.newLookup().from("favours") .localField("game_id") .foreignField("game_id") .as("games"); SkipOperation skipOperation = Aggregation.skip(skip); LimitOperation limitOperation = Aggregation.limit(size); return mongoTemplate.aggregate(Aggregation.newAggregation( lookupOperation, skipOperation, limitOperation),"entity",EntityResponse.class).getMappedResults(); }
核心原因
1. 仅Skip+Limit的高效逻辑
MongoDB的聚合管道是顺序执行的,当只有Skip+Limit时:
- 如果
entity集合有合适的索引(比如默认的_id索引),MongoDB会直接通过索引定位到需要跳过的位置,不需要扫描skip之前的所有文档。 - 本质是直接从索引中截取
skip到skip+size的范围,所以不管skip多大,耗时基本稳定。
2. 加入Lookup后的性能瓶颈
当把Lookup放在Skip+Limit前面时,执行顺序完全变了:
- 先对
entity集合的所有文档执行Lookup关联,和favours集合匹配game_id,生成包含games字段的全量中间结果集。 - 再对这个全量中间结果集执行Skip和Limit——skip越大,意味着要先处理完前面
skip条已经完成关联的文档,才能取后面的size条。 - 更糟的是,如果
favours集合的game_id没有建索引,每次Lookup都是全表扫描,单条关联的耗时就很高,skip越大,累计的关联操作次数越多,总耗时自然呈线性增长。
优化方案
1. 调整管道顺序(最有效)
把Skip+Limit移到Lookup前面,这样只对需要的size条文档执行关联,而不是全量处理:
public List<EntityResponse> findListOfEntities(int skip, int size) { SkipOperation skipOperation = Aggregation.skip(skip); LimitOperation limitOperation = Aggregation.limit(size); LookupOperation lookupOperation = LookupOperation.newLookup().from("favours") .localField("game_id") .foreignField("game_id") .as("games"); return mongoTemplate.aggregate(Aggregation.newAggregation( skipOperation, limitOperation, lookupOperation ),"entity",EntityResponse.class).getMappedResults(); }
2. 给关联字段加索引
给favours集合的game_id字段创建索引,大幅降低Lookup的关联耗时:
// MongoDB shell执行 db.favours.createIndex({game_id: 1})
3. 替换大Skip的分页方式
Skip本身在大数据量场景下性能就差,建议用基于游标/有序字段的分页,比如用最后一条文档的_id或者更新时间来定位下一页:
// 示例:以上一页最后一条的_id为条件,替代skip public List<EntityResponse> findListOfEntities(String lastId, int size) { MatchOperation matchOperation = Aggregation.match(Criteria.where("_id").gt(new ObjectId(lastId))); LimitOperation limitOperation = Aggregation.limit(size); LookupOperation lookupOperation = LookupOperation.newLookup().from("favours") .localField("game_id") .foreignField("game_id") .as("games"); return mongoTemplate.aggregate(Aggregation.newAggregation( matchOperation, limitOperation, lookupOperation ),"entity",EntityResponse.class).getMappedResults(); }
内容的提问来源于stack exchange,提问作者Jonnah.Jameson
相关产品推荐
相关产品推荐

