聚合框架中的$skip和$limit

66

当我阅读该文档时,我发现以下注释:

在管道中$limit紧接着$sort时,$sort操作只保留前n个结果,其中n是指定的限制数量,并且MongoDB只需要在内存中存储n个项。即使allowDiskUse为true并且n个项超过了聚合内存限制,这种优化仍然适用。

如果我的理解正确,那么它仅适用于我同时使用$sort和$limit的情况,例如

db.coll.aggregate([    ...,    {$sort: ...},    {$limit: limit},    ...]);

然而,我认为大部分时间我们都会

db.coll.aggregate([    ...,    {$sort: ...},    {$skip: skip},    {$limit: limit},    ...]);

问题1:如果我在这里使用$skip,那么上面的规则是否不适用?

我问这个问题是因为理论上MongoDB仍然可以计算出前n条记录,并通过仅对前n条记录进行排序来提高性能。虽然我没有找到任何相关文档。如果规则不适用,

问题2:我需要更改我的查询以提高性能吗?

db.coll.aggregate([    ...,    {$sort: ...},    {$limit: skip + limit},    {$skip: skip},    {$limit: limit},    ...]);

编辑:我认为解释我的使用情况会让上面的问题更有意义。我正在使用 MongoDB 2.6 提供的文本搜索功能来查找产品。我担心如果用户输入一个非常常见的关键字,比如“红色”,那么将返回太多结果。因此,我正在寻找更好的方法来生成这个结果。

编辑2: 原来上面的最后一行代码等于

db.coll.aggregate([    ...,    {$sort: ...},    {$limit: skip + limit},    {$skip: skip},    ...]);

因此,我们可以始终使用这种形式来应用前n个规则。

4个回答

90

既然我们要讨论的是文本搜索查询,那么最优形式就是这样:

db.collection.aggregate([
    { 
       "$match": {
               "$text": { "$search": "cake tea" }
    }
    },
    { "$sort": { "score": { "$meta": "textScore" } } },
    { "$limit": skip + limit },
    { "$skip": skip }
])

从“排序”结果的内存储备中提取数据只适用于其自身的“限制范围”,对于超出几个合理“页面”的数据将不会是最佳选择。

如果超过了合理的内存消耗,额外的阶段很可能会产生负面影响而非正面影响。

这些确实是MongoDB在当前形式下可用的文本搜索能力的实际限制。但如果需要更详细、更高性能的内容,则最好使用专门构建的外部文本搜索解决方案,就像许多SQL“全文”解决方案一样。


你说的是现有形式。你知道是否正在进行增强MongoDB文本搜索的工作吗?这里有一些关于在MongoDB中与Solr结合使用的绝妙评论:https://dev59.com/SHA75IYBdhLWcg3wlqIT。 - John Powell
@JohnBarça 你所寻找的答案实际上更为“官方”,并且有点带有主观色彩。在我看来,MongoDB承认自己并不试图成为一个“最佳”的键/值存储,也不试图实现传统关系系统作为“数据库”的每个功能。这一延伸意味着通用“数据库”通常不会涉及“文本搜索”等专业领域。但这只是一种观点,而观点往往是可以改变的。无论如何,请使用最适合您的工具。 - Neil Lunn
1
我一直在思考这个问题,现在我明白了为什么MongoDB只允许使用$limit而不是$skip来应用“top n”规则。因为skip+limit总是可以转换为limit+skip。我已经编辑了我的问题。 - yaoxing
@NeilLunn,您提到了“因为我们正在讨论文本搜索”,但是为了澄清,立即在“sort”后跟随“limit”的方法是最优方式,以便在排序后尽可能减少聚合链中的内存消耗。无论是通过展开数组进行文本搜索还是正常匹配,我们都可以从中获得少量的内存收益。这正确吗? - Sundar
@NeilLunn,这个问题可能有哪些解决方案?https://stackoverflow.com/questions/59573513/skip-and-limit-for-pagination-for-a-mongo-aggregate - KcH
显示剩余5条评论

14

答案:$skip在$limit之前

$skip和$limit的顺序至关重要,至少在聚合中是这样。我刚试过了,我不知道为什么之前会被忽略,也许自从那次操作以来它已经改变了,但我想分享一下我的经验。

我同意@vkarpov15在这个对话中的评论。

在聚合中,$limit限制将发送到下一个聚合状态的文档数量,而$skip跳过前N个文档,因此如果$skip在$limit之后且$skip>=$limit,则不会得到任何结果。简而言之,这是MongoDB中预期的行为。


2
哇,谢谢!我一直在苦苦挣扎 skiplimit,并且想知道为什么 MongoDB 表现得像那样(limit = limit + skip)!现在通过改变顺序,limit 的表现符合我的预期了。 - Positivity

9
我发现,似乎limitskip的顺序无关紧要。如果我在limit之前指定skip,MongoDB会在内部先执行limit再执行skip
> db.system.profile.find().limit(1).sort( { ts : -1 } ).pretty()
{
    "op" : "command",
    "ns" : "archiprod.userinfos",
    "command" : {
        "aggregate" : "userinfos",
        "pipeline" : [
            {
                "$sort" : {
                    "updatedAt" : -1
                }
            },
            {
                "$limit" : 625
            },
            {
                "$skip" : 600
            }
        ],
    },
    "keysExamined" : 625,
    "docsExamined" : 625,
    "cursorExhausted" : true,
    "numYield" : 4,
    "nreturned" : 25,
    "millis" : 25,
    "planSummary" : "IXSCAN { updatedAt: -1 }",
    /* Some fields are omitted */
}

如果我交换$skip$limit会发生什么?就keysExamineddocsExamined而言,我得到了相同的结果。

> db.system.profile.find().limit(1).sort( { ts : -1 } ).pretty()
{
    "op" : "command",
    "ns" : "archiprod.userinfos",
    "command" : {
        "aggregate" : "userinfos",
        "pipeline" : [
            {
                "$sort" : {
                    "updatedAt" : -1
                }
            },
            {
                "$skip" : 600
            },
            {
                "$limit" : 25
            }
        ],
    },
    "keysExamined" : 625,
    "docsExamined" : 625,
    "cursorExhausted" : true,
    "numYield" : 5,
    "nreturned" : 25,
    "millis" : 71,
    "planSummary" : "IXSCAN { updatedAt: -1 }",
}

我随后检查了查询的解释结果。我发现在limit阶段,totalDocsExamined已经是625了。

> db.userinfos.explain('executionStats').aggregate([ { "$sort" : { "updatedAt" : -1 } }, { "$limit" : 625 }, { "$skip" : 600 } ])
{
    "stages" : [
        {
            "$cursor" : {
                "sort" : {
                    "updatedAt" : -1
                },
                "limit" : NumberLong(625),
                "queryPlanner" : {
                    "winningPlan" : {
                        "stage" : "FETCH",
                        "inputStage" : {
                            "stage" : "IXSCAN",
                            "keyPattern" : {
                                "updatedAt" : -1
                            },
                            "indexName" : "updatedAt_-1",
                        }
                    },
                },
                "executionStats" : {
                    "executionSuccess" : true,
                    "nReturned" : 625,
                    "executionTimeMillis" : 22,
                    "totalKeysExamined" : 625,
                    "totalDocsExamined" : 625,
                    "executionStages" : {
                        "stage" : "FETCH",
                        "nReturned" : 625,
                        "executionTimeMillisEstimate" : 0,
                        "works" : 625,
                        "advanced" : 625,
                        "docsExamined" : 625,
                        "inputStage" : {
                            "stage" : "IXSCAN",
                            "nReturned" : 625,
                            "works" : 625,
                            "advanced" : 625,
                            "keyPattern" : {
                                "updatedAt" : -1
                            },
                            "indexName" : "updatedAt_-1",
                            "keysExamined" : 625,
                        }
                    }
                }
            }
        },
        {
            "$skip" : NumberLong(600)
        }
    ]
}

令人惊讶的是,我发现交换$skip$limit得到的explain结果相同。

> db.userinfos.explain('executionStats').aggregate([ { "$sort" : { "updatedAt" : -1 } }, { "$skip" : 600 }, { "$limit" : 25 } ])
{
    "stages" : [
        {
            "$cursor" : {
                "sort" : {
                    "updatedAt" : -1
                },
                "limit" : NumberLong(625),
                "queryPlanner" : {
                    /* Omitted */
                },
                "executionStats" : {
                    "executionSuccess" : true,
                    "nReturned" : 625,
                    "executionTimeMillis" : 31,
                    "totalKeysExamined" : 625,
                    "totalDocsExamined" : 625,
                    /* Omitted */
                }
            }
        },
        {
            "$skip" : NumberLong(600)
        }
    ]
}

如您所见,尽管我在$skip之前指定了$limit,但在explain结果中,仍然是$limit$skip之前。


如何使$limit不可更改。因为我需要在循环中进行测试,每次循环时,当我使用一个常量增加skip时,由于$limit始终为$skip + $limit,每个请求(TTFB)的执行时间都会增加。我们该如何解决这个问题? - O.k

0
简单来说,我得出的结论是这并不重要。假设查询中没有过滤器,首先跳过10个文档,然后限制为5个文档,如果解释查询,将返回最后5个文档并总共检查15个文档。请评价我的分析。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接