MongoDB 2.2 聚合框架按字段名称分组

3

能否按字段名称分组?还是说我需要不同的结构以便可以按值进行分组?

我知道我们可以在值上使用group by,也可以展开数组,但是是否可能在这里获取John拥有的三个房屋中的苹果、梨子和桔子的总数,而不需要在查询中明确指定“苹果”、“梨子”和“桔子”?(所以不像这样);

// total all the fruit John has at each house
db.houses.aggregate([
    {
        $group: {
            _id: null,

            "apples":  { $sum: "$people.John.items.apples" },
            "pears":   { $sum: "$people.John.items.pears" }, 
            "oranges": { $sum: "$people.John.items.oranges" }, 
        }
    },
])

换句话说,我可以按照“items”下的第一个字段名称进行分组,并获得苹果:104,梨子:202和橙子:306的聚合总和,还有香蕉、甜瓜和其他可能存在的任何东西吗?还是需要将数据重构为类别的键值对数组?
db.createCollection("houses");
db.houses.remove();
db.houses.insert(
[
    {
        House: "birmingham",
        categories : [
            {
                k : "location",
                v : { d : "central" }
            }
        ],
        people: {
            John: {
                items: {
                    apples: 2,
                    pears: 1,
                    oranges: 3,
                }
            },
            Dave: {
                items: {
                    apples: 30,
                    pears: 20,
                    oranges: 10,
                },
            },
        },
    },
    {
        House: "London", categories: [{ k: "location", v: { d: "central" } }, { k: "type", v: { d: "rented" } }],
        people: {
            John: { items: { apples: 2, pears: 1, oranges: 3, } },
            Dave: { items: { apples: 30, pears: 20, oranges: 10, }, },
        },
    },
    {
        House: "Cambridge", categories: [{ k: "type", v: { d: "rented" } }],
        people: {
            John: { items: { apples: 100, pears: 200, oranges: 300, } },
            Dave: { items: { apples: 0.3, pears: 0.2, oranges: 0.1, }, },
        },
    },
]
);

其次,更重要的是,我是否可以按“house.categories.k”进行分组?换句话说,是否可以找出“John”在“rented”与“owned”或“friends”房屋中拥有多少“apples”(因此按“categories.k.type”分组)?
最后,如果这样做是可行的,那么它是否明智?起初,我认为使用对象的实际字段名称创建嵌套对象的字典非常有用,因为它似乎是文档数据库的逻辑用法,并且似乎比数组更容易编写MR查询,但现在我开始怀疑这是否都是一个坏主意,并且具有可变字段名称使得编写聚合查询非常棘手/低效。
3个回答

3

好的,我认为我已经部分解决了这个问题。至少对于最初问题中的数据形状来说是这样的。


注:本文中的 HTML 标签已保留。
// How many of each type of fruit does John have at each location
db.houses.aggregate([
    {
        $unwind: "$categories"
    },
    {
        $match: { "categories.k": "location" }
    },
    {
        $group: {
            _id: "$categories.v.d",
            "numberOf": { $sum: 1 },
            "Total Apples": { $sum: "$people.John.items.apples" },
            "Total  Pears": { $sum: "$people.John.items.pears" },
        }
    },
])

产生如下结果;

{
        "result" : [
                {
                        "_id" : "central",
                        "numberOf" : 2,
                        "Total Apples" : 4,
                        "Total  Pears" : 2
                }
        ],
        "ok" : 1
}

请注意,只有“central”,但如果我的数据库中有其他“位置”,我将获得每个位置的总计范围。如果我有命名属性而不是“类别”数组,我就不需要$unwind步骤,但这就是我发现结构与自身相矛盾的地方。在“类别”下可能有几个关键字。样本数据显示“类型”和“位置”,但可能有大约10个这些分类,每个都具有不同的值。因此,如果我使用命名字段;
"categories": {
  location: "london",
  type: "owned",
}

...我面临的问题是索引。我无法只索引“位置”,因为那些是用户定义的类别,如果有10000个用户选择分类他们的房屋的10000种不同方式,我需要10000个索引,每个字段一个。但是通过将其制作成数组,我只需要一个数组字段上的一个索引。缺点是$unwind步骤。我以前在MapReduce中遇到过这个问题。你最不想做的事情就是用JavaScript中的ForEach循环来循环数组,如果可以避免,就要尽量避免。你真正想要的是按名称过滤字段,因为这样更快。

现在对于我已经知道我要找的水果的情况,这很好,但如果我不知道,情况就会更难。我无法(据我所知)在这里展开或以其他方式循环“people.John.items”。如果可以的话,我会非常高兴。因此,由于水果的名称再次是用户定义的,看起来我也需要将它们转换为数组,像这样:

{
    "people" : {
        "John" : {
            "items" : [
                { k:"apples", v:100 },
                { k:"pears", v:200 },
                { k:"oranges", v:300 },
            ]
        },
    }
}

现在,我可以通过位置获取水果的总数(即使我不知道要找哪种水果);

db.houses.aggregate([
    {
        $unwind: "$categories"
    },
    {
        $match: { "categories.k": "location" }
    },
    {
        $unwind: "$people.John.items" 
    },
    {
        $group: { // compound key - thanks to Jenna
            _id: { fruit:"$people.John.items.k", location:"$categories.v.v" },
            "numberOf": { $sum: 1 },
            "Total Fruit": { $sum: "$people.John.items.v" },
        }
    },
])

现在我要使用两个$unwind。如果你认为这看起来非常低效,那么你是对的。如果我只有10,000个房屋记录,每个记录有10个类别和10种水果,这个查询需要半分钟才能运行。 好的,我可以看到将$match放在$unwind之前可以显著改善情况,但这是错误的输出。我不想为每个类别创建条目,我只想筛选出“位置”类别。


这甚至不是我想要的输出。我并不真正想要一个由{水果名称和位置}组成的复合键,实际上我更喜欢在一个位置有苹果、梨子等值的数组,然后在其他位置有另一个(苹果、梨子等)数组。 - cirrus

2

我本来想在评论里写这个,但在回复文本框里更容易进行格式化。

{ _id: 1,
  house: "New York",
  people: {
      John: {
          items: {apples: 1, oranges:2}
      }
      Dave: {
          items: {apples: 2, oranges: 1}
      }
  }
}

{ _id: 2,
      house: "London",
      people: {
          John: {
              items: {apples: 3, oranges:2}
          }
          Dave: {
              items: {apples: 1, oranges:3}
          }
      }
}

只是为了确保我理解你的问题,你想要实现的是这个吗?

{location: "New York", johnFruit:3}
{location: "London", johnFruit: 5}

由于categories不是嵌套在house下面的,所以您无法按“house.categories.k”进行分组,但是您可以使用一个复合键作为$group的_id来获得此结果:

{ $group: _id: {house: "$House", category: "$categories.k"} 

虽然 "k" 不包含你想要分组的信息。对于 "categories.k.type",type 是 k 的值,因此你不能使用这种语法。你需要按 "categories.v.d" 进行分组。

使用当前的模式结构,可能可以通过 $unwind、$project、可能的 $match 和最后 $group 来完成此聚合,但命令不会很好看。如果可能的话,我强烈建议重新构造数据以使此聚合更加简单。如果你需要关于模式的帮助,请告诉我们。


是的,我也是这么想的。对于这种查询,我认为将项目设置为[{k:“苹果”,v:“30”},…]会更好。 - cirrus
顺便说一句,我非常希望在模式方面得到一些帮助。 - cirrus
你能给出aggregate()函数的输出示例吗?例如{location: x, totalFruit: y}或{location: x, typeOfHouse: y, johnOrange: z}等。 - Jenna
另外,您可以发布您最常见的查询,并可能告诉我们一些关于您的应用程序的信息吗?查询应该有助于决定文档结构和索引创建。 - Jenna
一些关键查询如下:1)在位置为“中央”的每个房子中,具有“ISBN-10:067003469X”的所有书籍的总价值 2)返回John在每个房子中拥有的(特定)苹果、橙子的总数的数组(因此忽略香蕉),其中位置为“中央”,可以包含或不包含水果名称 3)返回John在每个房子中拥有的任何水果(未指定的香蕉、甜瓜等),其中位置为“中央”的总数的数组,因此需要水果名称 - cirrus
显示剩余6条评论

0

我不确定这是否是一个可行的解决方案,但如果您使用distinct()确定不同位置的数量,并针对每个位置运行单独的聚合命令,那么是否可以开始聚合过程呢?distinct()可能不是很高效,但是每个后续的聚合都将能够使用$match,因此可以使用类别上的索引。您可以使用相同的逻辑来计算“categories.type”的水果数量。

{
    "_id" : 1,
    "house" : "New York",
    "people" : {
        "John" : [{"k" : "apples","v" : 1},{"k" : "oranges","v" : 2}],
        "Dave" : [{"k" : "apples","v" : 2},{"k" : "oranges","v" : 1}]
    },
    "categories" : [{"location" : "central"},{"type" : "rented"}]
}
{
    "_id" : 2,
    "house" : "London",
    "people" : {
        "John" : [{"k" : "apples","v" : 3},{"k" : "oranges","v" : 2}],
        "Dave" : [{"k" : "apples","v" : 3},{"k" : "oranges","v" : 1}]
    },
    "categories" : [{"location" : "suburb"},{"type" : "rented"}]
}
{
    "_id" : 3,
    "house" : "London",
    "people" : {
        "John" : [{"k" : "apples","v" : 0},{"k" : "oranges","v" : 1}],
        "Dave" : [{"k" : "apples","v" : 2},{"k" : "oranges","v" : 4}]
    },
    "categories" : [{"location" : "central"},{"type" : "rented"}]
}

运行 distinct(),通过为每个唯一的“categories.location”值运行 aggregate() 命令来迭代结果:

db.agg.distinct("categories.location")
[ "central", "suburb" ]

db.agg.aggregate(
    {$match: {categories: {location:"central"}}}, //the index entry is on the entire 
    {$unwind: "$people.John"},                    //document {location:"central"}, so 
    {$group:{                                     //use this syntax to use the index
         _id:"$people.John.k", 
         "numberOf": { $sum: 1 },
         "Total Fruit": { $sum: "$people.John.v"}
        }
     }
 )


{
    "result" : [
        {
            "_id" : "oranges",
            "numberOf" : 2,
            "Total Fruit" : 3
        },
        {
            "_id" : "apples",
            "numberOf" : 2,
            "Total Fruit" : 1
        }
    ],
    "ok" : 1
}

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