You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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前面时,执行顺序完全变了:

  1. 先对entity集合的所有文档执行Lookup关联,和favours集合匹配game_id,生成包含games字段的全量中间结果集。
  2. 再对这个全量中间结果集执行Skip和Limit——skip越大,意味着要先处理完前面skip条已经完成关联的文档,才能取后面的size条。
  3. 更糟的是,如果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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 05:17:27