如何按不同领域分组

Mic*_* K. 6 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"
}
Run Code Online (Sandbox Code Playgroud)

结果应该是这样的:

"result" : 
[
  { 
    "age" : 
      [
        {
          "value" : "50",
          "count" : "1"
        },
        {
          "value" : "40",
          "count" : "2"
        }
      ]
  },
  { 
    "childs" : 
      [
        {
          "value" : "2",
          "count" : "2"
        },
        {
          "value" : "1",
          "count" : "1"
        }
      ]
  }  
]
Run Code Online (Sandbox Code Playgroud)

我怎样才能做到这一点?

小智 6

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

这里的基本原则是"MongoDB不做连接".任何"构想"如何构造SQL来执行此操作的方式基本上都需要"连接"操作.典型的形式是"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"
        }}
    }}
])
Run Code Online (Sandbox Code Playgroud)

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

{
    "_id" : null,
    "age" : [
            {
                    "value" : "50",
                    "count" : 1
            },
            {
                    "value" : "40",
                    "count" : 3
            }
    ],
    "childs" : [
            {
                    "value" : "2",
                    "count" : 3
            },
            {
                    "value" : "1",
                    "count" : 1
            }
    ]
}
Run Code Online (Sandbox Code Playgroud)

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

    { "$group": {
        "_id": null,
        "age": { "$push": "$age" },
        "childs": { "$push": "$childs" }
    }},
Run Code Online (Sandbox Code Playgroud)

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

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

您可以通过执行"复制"文档"类型""年龄或"孩子"以及按类型单独分组文档等方式来避免这种情况.但这有点"过于复杂"而不是一种坚实的方式做事.

自然的反应是"UNION怎么样?",但是由于MongoDB不进行"加入",那么如何处理呢?


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

在架构和性能方面,您最好的方法是通过客户端API以"并行"方式向服务器提交"两个"查询(是两个).收到结果后,您可以将它们"组合"成一个响应,然后将其作为数据源发送回最终的"客户端"应用程序.

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

我的示例目的node.js在于使用"异步"方面基本上是"内置"并且相当直观地遵循.事物的"组合"方面可以是任何类型的"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 ) );
    }
  );
});
Run Code Online (Sandbox Code Playgroud)

这给出了可爱的结果:

{
  "age": [
    {
      "value": "50",
      "count": 1
    },
    {
      "value": "40",
      "count": 3
    }
  ],
  "childs": [
    {
      "value": "2",
      "count": 3
    },
    {
      "value": "1",
      "count": 1
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

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


关键点在这里.

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

  • 使用支持"并行"和"异步"执行的语言实现和API,这样您就可以立即"加载"所有或"大部分"操作.

  • API应该支持某种"组合"方法,或者允许单独的"流"写入来处理接收到的一个结果集.

  • 忘记SQL的方式.NoSQL方式将"连接"等事务的处理委托给您的"数据逻辑层",这是包含此处所示代码的内容.它是这样做的,因为它可以扩展到非常大的数据集.相反,您的"数据逻辑"处理大型应用程序中的节点需要将其提供给最终API.

与我可能描述的任何其他形式的"争吵"相比,这是快速的."NoSQL"思想的一部分是"忘掉你所学到的东西"并以不同的方式看待事物.如果这种方式表现不佳,那么坚持使用SQL方法进行存储和查询.

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

  • @SylvainLeroux如果该集合实际上是同时更新的,那么无论采用何种方法,您都可以获得不同的结果.锁定MongoDB是一个"嗯"的问题.所以我会留下它.真正"快照"任何东西都很难,所以这基本上是另一个非常冗长的博客文章(或系列). (3认同)