如何按不同字段进行分组

时间:2015-06-09 11:32:50

标签: mongodb aggregation-framework

我想找到所有名为'Hans'的用户,并通过分组来汇总他们的'年龄'和'孩子'的数量。 假设我在我的数据库中有“用户”。

{
    "_id" : "01",
    "user" : "Hans",
    "age" : "50"
    "childs" : "2"
}
{
    "_id" : "02",
    "user" : "Hans",
    "age" : "40"
    "childs" : "2"
}
{
    "_id" : "03",
    "user" : "Fritz",
    "age" : "40"
    "childs" : "2"
}
{
    "_id" : "04",
    "user" : "Hans",
    "age" : "40"
    "childs" : "1"
}

结果应该是这样的:

"result" : 
[
  { 
    "age" : 
      [
        {
          "value" : "50",
          "count" : "1"
        },
        {
          "value" : "40",
          "count" : "2"
        }
      ]
  },
  { 
    "childs" : 
      [
        {
          "value" : "2",
          "count" : "2"
        },
        {
          "value" : "1",
          "count" : "1"
        }
      ]
  }  
]

我怎样才能做到这一点?

2 个答案:

答案 0 :(得分:6)

这几乎应该是一个MongoDB常见问题解答,主要是因为它是一个真实的例子,说明你应该如何改变你对SQL处理的想法,并接受像MongoDB这样的引擎。

这里的基本原则是" MongoDB不做连接"。任何方式设想"如何构建SQL来实现这一点基本上需要一个" join"操作。典型的形式是" UNION"实际上是一个"加入"。

那么如何在不同的范例下做到这一点呢?首先,让我们来解决这样做的原因并理解原因。即使它当然适用于您的小样本:

困难之路

db.docs.aggregate([
    { "$group": {
        "_id": null,
        "age": { "$push": "$age" },
        "childs": { "$push": "$childs" }
    }},
    { "$unwind": "$age" },
    { "$group": {
        "_id": "$age",
        "count": { "$sum": 1  },
        "childs": { "$first": "$childs" }
    }},
    { "$sort": { "_id": -1 } },
    { "$group": {
        "_id": null,
        "age": { "$push": {
            "value": "$_id",
            "count": "$count"
        }},
        "childs": { "$first": "$childs" }
    }},
    { "$unwind": "$childs" },
    { "$group": {
        "_id": "$childs",
        "count": { "$sum": 1 },
        "age": { "$first": "$age" }
    }},
    { "$sort": { "_id": -1 } },
    { "$group": {
        "_id": null,
        "age": { "$first": "$age" },
        "childs": { "$push": {
            "value": "$_id",
            "count": "$count"
        }}
    }}
])

这会给你一个这样的结果:

{
    "_id" : null,
    "age" : [
            {
                    "value" : "50",
                    "count" : 1
            },
            {
                    "value" : "40",
                    "count" : 3
            }
    ],
    "childs" : [
            {
                    "value" : "2",
                    "count" : 3
            },
            {
                    "value" : "1",
                    "count" : 1
            }
    ]
}

那为什么这么糟糕?在第一个流水线阶段,主要问题应该是显而易见的:

    { "$group": {
        "_id": null,
        "age": { "$push": "$age" },
        "childs": { "$push": "$childs" }
    }},

我们要求在这里做的是将集合中的所有分组为我们想要的值,并将$push这些结果分组到一个数组中。当事情很小,那么这是有效的,但真实的世界收藏将导致这个"单个文件"在管道中超过允许的16MB BSON限制。这就是坏事。

其余的逻辑遵循自然过程,通过使用每个数组。但当然,现实世界的场景几乎总会让这种情况变得难以为继。

你可以通过做"复制"等事情来避免这种情况。文件是"类型" "年龄或"孩子"并按类型分类文档。但是,对于复杂的"而不是一种坚实的做事方式。

自然反应是" UNION怎么样?",但是因为MongoDB不做"加入"然后如何处理?

更好的方式(又名新希望)

您在建筑和性能方面的最佳方法是简单地提交"两者" " parallel"中的查询(是2)通过您的客户端API到服务器。收到结果后,你就可以"结合"将它们转换为单个响应,然后您可以将其作为数据源发送回最终的"客户端"应用

不同的语言有不同的方法,但一般情况是寻找一个"异步处理"允许您串联执行此操作的API。

我的示例目的是使用node.js作为"异步"方基本上是#34;内置"并且相当直观地遵循。 "组合"事物的一面可以是任何类型的" hash / map / dict"表实现,只是简单的方式,例如:

var async = require('async'),
    MongoClient = require('mongodb');

MongoClient.connect('mongodb://localhost/test',function(err,db) {

  var collection = db.collection('docs');

  async.parallel(
    [
      function(callback) {
        collection.aggregate(
          [
            { "$group": {
              "_id": "$age",
              "type": { "$first": { "$literal": "age" } },
              "count": { "$sum": 1 }
            }},
            { "$sort": { "_id": -1 } }
          ],
          callback
        );
      },
      function(callback) {
        collection.aggregate(
          [
            { "$group": {
              "_id": "$childs",
              "type": { "$first": { "$literal": "childs" } },
              "count": { "$sum": 1 }
            }},
            { "$sort": { "_id": -1 } }

          ],
          callback
        );
      }
    ],
    function(err,results) {
      if (err) throw err;
      var response = {};
      results.forEach(function(res) {
        res.forEach(function(doc) {
          if ( !response.hasOwnProperty(doc.type) )
            response[doc.type] = [];

          response[doc.type].push({
            "value": doc._id,
            "count": doc.count
          });
        });
      });

      console.log( JSON.stringify( response, null, 2 ) );
    }
  );
});

这给出了可爱的结果:

{
  "age": [
    {
      "value": "50",
      "count": 1
    },
    {
      "value": "40",
      "count": 3
    }
  ],
  "childs": [
    {
      "value": "2",
      "count": 3
    },
    {
      "value": "1",
      "count": 1
    }
  ]
}

所以这里要注意的关键是"分开"聚合语句本身实际上非常简单。你唯一面对的是将最终结果中的那些结合起来。有许多方法可以组合"特别是处理来自每个查询的大量结果,但这是执行模型的基本示例。

这里的关键点。

  • 可以在聚合管道中混洗数据,但不适用于大型数据集。

  • 使用支持" parallel"的语言实现和API。和"异步"执行所以你可以加载"全部或者大多数"你的业务一次性。

  • API应该支持"组合的一些方法"或以其他方式允许单独的"流"写入处理收到的每个结果集。

  • 忘记SQL方式。 NoSQL方式委托处理诸如"加入"到你的"数据逻辑层",它包含这里显示的代码。它是这样做的,因为它可以扩展到非常大的数据集。这是你的数据逻辑"的工作。处理大型应用程序中的节点以将其传递到最终API。

与任何其他形式的"争吵相比, 我可以描述一下。部分" NoSQL"思考是“忘掉你学到的东西”#34;并以不同的方式看待事物。如果这种方式没有更好的表现,那么坚持使用SQL方法进行存储和查询。

这就是替代品存在的原因。

答案 1 :(得分:2)

是一个艰难的人!

首先,裸解决方案:

db.test.aggregate([
 { "$match": { "user": "Hans" } },
 // duplicate each document: one for "age", the other for "childs"
 { $project: { age: "$age", childs: "$childs",
               data: {$literal: ["age", "childs"]}}},
 { $unwind: "$data" },
 // pivot data to something like { data: "age", value: "40" }
 { $project: { data: "$data",
               value: {$cond: [{$eq: ["$data", "age"]},
                               "$age", 
                               "$childs"]} }},
 // Group by data type, and count
 { $group: { _id: {data: "$data", value: "$value" }, 
             count: { $sum: 1 }, 
             value: {$first: "$value"} }},
 // aggregate values in an array for each independant (type,value) pair
 { $group: { _id: "$_id.data", values: { $push: { count: "$count", value: "$value" }} }} ,
 // project value to the correctly name field
 { $project: { result: {$cond: [{$eq: ["$_id", "age"]},
                               {age: "$values" }, 
                               {childs: "$values"}]} }},
 // group all data in the result array, and remove unneeded `_id` field 
 { $group: { _id: null, result: { $push: "$result" }}},
 { $project: { _id: 0, result: 1}}
])

产:

{
    "result" : [
        {
            "age" : [
                {
                    "count" : 3,
                    "value" : "40"
                },
                {
                    "count" : 1,
                    "value" : "50"
                }
            ]
        },
        {
            "childs" : [
                {
                    "count" : 1,
                    "value" : "1"
                },
                {
                    "count" : 3,
                    "value" : "2"
                }
            ]
        }
    ]
}

现在,有些解释:

这里的一个主要问题是每个传入的文档必须是两个不同总和的一部分。我通过向文档添加文字数组["age", "childs"],然后通过该数组展开它来解决这个问题。这样,每个文档将在后期中显示两次

完成后,为了简化处理,我将数据表示更改为更易于管理的内容,例如{ data: "age", value: "40" }

以下步骤将执行数据聚合本身。最多第三个$project步骤,将值字段映射到相应的agechilds字段。

最后两个步骤将简单地将两个文档合并为一个,删除不需要的_id字段。

Pfff!