是否可以在 mongodb 中按四分位数有效聚合?

Is it possible to aggregate efficiently by quartile in mongodb?

例如,假设我有 10,000 个已排序的文档要 aggregate()。但我想将它们分为四分位数:前 25%、25% - 50%、50% - 75%、后 25%。有没有一种方法可以在一个管道中完成此操作,而不必为每个四分位数执行 4 个单独的管道?

类似于:

aggregate()
- Transform into {quartile1: [list of docs], quartile2: [list of docs], ...}
- Run other pipeline commands

或者我需要运行 4 个单独的 aggregate() 管道吗?

谢谢!

对于您所问的问题,"can the aggregation framework to this?",那么答案是否定的。另一方面,您可以使用 mapReduce 做类似的事情。但我真正想展示的是它的可靠性,此外还有 "what would be the point?".

在这里表达怀疑的最好方法是充分解释事情。

Aggregation 框架不能做这种事情,因为它在处理您的 10,000 个文档的过程中没有 "where it was currently" 的概念。为此,您需要某种 "variable",它会随着每个 "sorted" 项目的处理而递增。

您可以根据您 "sorting" 的值使用 "tag" 项目的方法。但问题仍然是 "how would you know" 特定值在整个结果集中的排名。因此,除非有明确的方法可以做到这一点,否则您无法投射这样的领域。

只有当您准备使用不一定是所有结果的 "quarter division" 的 "set range" 时,您才能使用 .aggregate():

db.collection.aggregate([
    { "$project": {
        "grouping": {
            "$cond": [
                { "$lt": [ "$score", 25 ]  },
                3,
                { "$cond": [
                    { "$lt": [ "$score", 50 ] },
                    2,
                    { "$cond": [
                        { "$lt": [ "$score", 75 ] },
                        1,
                        0
                    ]}
                ]}
            ]
        },
        "score": 1,
        "otherField": 1
    }},
    { "$sort": { "grouping"  1, "score": -1 }
])

另一方面,.mapReduce() 确实可以访问这样的全局变量。因此基本上可以检查一个计数器以查看它是否在您预期的分组中。基本形式:

db.collection.mapReduce(
    function() {
        counter++;
        if ( counter % ( total / 4 ) == 0 )
            grouping++;

        var id = this._id;
        delete this._id;

        emit({ "grouping": grouping, "_id": id },this);
    },
    function() {}, // no need for a reducer
    {
        "out": { "replace": "results" },
        "scope": { "counter": 0, "grouping": 0, "total": 10000 },
        "sort": { "score": -1 }
    }
)

它基本上做你想做的。但不是以一种真正灵活或非常可靠的方式。主要是因为在大多数现实世界的情况下,不能保证总是有 10,000 个结果,通常特别是如果 运行 一个查询有条件获得计数而另一个查询 "tag" 结果进入他们的分组。

因此,考虑到这里根本没有真正的 "aggregation" 发生,那么最好的方法可能是简单地查询数据并将其列出:

var cursor = db.collection.find({}).sort({ "score": -1 });
var total = cursor.count();

var counter = 0,
    grouping = 0;

cursor.forEach(function(doc) {
    counter++;
    if ( counter % ( total / 4 ) == 0 )
        grouping++;
    doc._id = { "grouping": grouping, "_id": doc._id };

    // Do something with "doc"
});

不是很优雅,但指出了基本技术。

另请注意,您建议的数组 [] 并不是一个好主意。即使在 10,000 个文档的情况下,在单个文档响应中生成的 2,500 个元素数组和实质上 10,000 个项目也可能达到 "blow up" 16MB BSON 限制。最起码不太好管理,还是用游标处理比较好。

因此,您可以选择 "tag" 这些项目的服务器,或者在阅读时 "tag" 它们。至少在后一种情况下,您可以访问 "cursor" 结果

我认为需要 4 个管道,与 mongo docs 中的管道一致。

db.articles.aggregate( [
                        { $match : { score : { $gt : 70, $lte : 90 } } },
                        { $group: { _id: null, count: { $sum: 1 } } }
                       ] );

但是对于所有正常的数据库规则,请考虑两次浸入或输入数据两次。一次用于基数字段,一次用于四分位数字段。这种方法很糟糕,但可以快速阅读;可以在索引字段上执行简单查找并执行单个聚合。

{name: cartman, score: 56, quartile: 3 }
{name: kenny, score: 36, quartile: 2 }
{name: kyle, score: 76, quartile: 4 }

db.scores.find( {"quartile" : 3 });

db.scores.aggregate( [
                        { $group: { _id: null, count: { $quartile: 1 } } }
                       ] );