在MongoDB中对数组中的子文档进行聚合

5
我正在使用mongodb作为后端实现一个小应用程序。在这个应用程序中,我的数据结构将包含一个字段,其中文档将包含一个子文档数组。
我使用以下用例作为基础: http://docs.mongodb.org/manual/use-cases/inventory-management/ 正如您从示例中看到的那样,每个文档都有一个名为carted的字段,它是一个子文档数组。
{
    _id: 42,
    last_modified: ISODate("2012-03-09T20:55:36Z"),
    status: 'active',
    items: [
        { sku: '00e8da9b', qty: 1, item_details: {...} },
        { sku: '0ab42f88', qty: 4, item_details: {...} }
    ]
}

这很适合我,除了一个问题: 我想在整个集合中计算每个唯一项目(以“sku”作为唯一标识符键),其中每个文档将计数加1(同一文档中的多个相同“sku”实例仍然只计数1)。例如,我希望得到以下结果:
{ sku: '00e8da9b', doc_count: 1 }, { sku: '0ab42f88', doc_count: 9 }
阅读了MongoDB的相关资料后,我对如何在具有上述复杂结构的情况下快速完成此操作感到困惑。如果我正确理解了非常好的文档,那么可能可以使用聚合框架或映射/减少框架来实现此操作,但这就是我需要一些输入的地方:
- 鉴于结构的复杂性,哪种框架更适合实现我正在寻找的结果? - 为了从所选框架中获得最佳性能,应该选择什么样的索引?
2个回答

15

MapReduce很慢,但它可以处理非常大的数据集。另一方面,聚合框架速度稍快,但在处理大数据量时会遇到困难。

你所展示的结构问题在于需要"$unwind"数组以打开数据。这意味着为每个数组项创建一个新文档,并且使用聚合框架需要在内存中执行此操作。因此,如果您有1000个包含100个数组元素的文档,则需要构建100,000个文档流才能对其进行groupBy和计数。

您可能想考虑查看是否有更好的模式布局适合您的查询,但如果要使用聚合框架进行操作,下面是如何实现(使用一些示例数据,使整个脚本都可以在shell中运行);

db.so.remove();
db.so.ensureIndex({ "items.sku": 1}, {unique:false});
db.so.insert([
    {
        _id: 42,
        last_modified: ISODate("2012-03-09T20:55:36Z"),
        status: 'active',
        items: [
            { sku: '00e8da9b', qty: 1, item_details: {} },
            { sku: '0ab42f88', qty: 4, item_details: {} },
            { sku: '0ab42f88', qty: 4, item_details: {} },
            { sku: '0ab42f88', qty: 4, item_details: {} },
    ]
    },
    {
        _id: 43,
        last_modified: ISODate("2012-03-09T20:55:36Z"),
        status: 'active',
        items: [
            { sku: '00e8da9b', qty: 1, item_details: {} },
            { sku: '0ab42f88', qty: 4, item_details: {} },
        ]
    },
]);


db.so.runCommand("aggregate", {
    pipeline: [
        {   // optional filter to exclude inactive elements - can be removed    
            // you'll want an index on this if you use it too
            $match: { status: "active" }
        },
        // unwind creates a doc for every array element
        { $unwind: "$items" },
        {
            $group: {
                // group by unique SKU, but you only wanted to count a SKU once per doc id
                _id: { _id: "$_id", sku: "$items.sku" },
            }
        },
        {
            $group: {
                // group by unique SKU, and count them
                _id: { sku:"$_id.sku" },
                doc_count: { $sum: 1 },
            }
        }
    ]
    //,explain:true
})
请注意,我$group'd两次,因为您说一个SKU在每个文档中只能计算一次,因此我们需要首先解决唯一的doc/sku对,然后再进行计数。
如果你想要输出稍微不同(换句话说,与你的样本完全相同),我们可以$project它们。

好的,我明天会尝试您的输入。您解释正在发生的事情真的很好。MongoDB聚合查询可能有点难以阅读。对于我的使用,我猜我将拥有大约60,000个文档,其中包含大约400,000个分布在文档中的项目。 - agnsaft
有了那种数量级,我怀疑你会想要尽可能地预先计算,这意味着使用MR而不是AF。除非你需要实时查询并且可以预先计算,否则使用MR会更好。 - cirrus

3

使用最新的Mongo构建(其他构建可能也是如此),我发现Cirrus答案的略微不同版本执行速度更快,消耗的内存更少。我不知道为什么会这样,似乎这个版本让Mongo有更多优化管道的可能性。

db.so.runCommand("aggregate", {
    pipeline: [
        { $unwind: "$items" },
        {
            $group: {
                // create array of unique sku's (or set) per id
                _id: { id: "$_id"},
                sku: {$addToSet: "$items.sku"}
            }
        },
        // unroll all sets
        { $unwind: "$sku" },
        {
            $group: {
                // then count unique values per each Id
                _id: { id: "$_id.id", sku:"$sku" },
                count: { $sum: 1 },
            }
        }
    ]
})

为了完全匹配问题中要求的格式,应该跳过按“_id”分组。

是因为你没有根据状态进行匹配吗? - cirrus
我不这么认为(我对自己的数据进行了性能测量,使用了相同的方法。为了简单起见,这里省略了$match)。实际上,如果正确索引,$matching甚至可以提高性能,因为它可以缩小后续步骤的数据量。我认为这是因为一个大的“$group”比几个较小的管道阶段更难优化Mongo。 - Volodymyr Metlyakov
是的,那似乎很有道理。 - cirrus

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