【问题标题】:How to group by different fields如何按不同的领域分组
【发布时间】:2015-08-24 04:46:40
【问题描述】:

我想找到所有名为“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"
        }
      ]
  }  
]

我怎样才能做到这一点?

【问题讨论】:

  • 您最好的方法是并行运行两个查询。除了非常小的数据样本之外,这是唯一实用的方法。作为一个例子来展示这将是一个很好的答案,但非常冗长。 “小数据”的愚蠢方法是推送到数组并单独处理。但这不适用于大多数“现实世界”数据集。
  • @user3561036 如果不强制使用聚合框架,我认为使用 map-reduce 将是一个选项。在这种特殊情况下,可能(?)对于更大的数据集比其他更人为的解决方案具有更好的性能。
  • @SylvainLeroux 同样的问题也适用。为了通常“按两个或更多事物分组”,您需要以组合形式表示每个事物的不同数据集。在 SQL 术语中,这基本上是一个“UNION”,但一个 union 本质上是一个“join”操作,因此 MongoDB 不这样做。就像我说的,有一些“愚蠢”的方式,但它们无法扩展。聪明的钱是在单独的“并行”查询上,并将结果组合起来发送到你的 API 中。通过这种方法,它们快速并且可扩展。就像我说的,这将是一个很好的常见问题解答,但解释很长。
  • @user3561036 这听起来很有趣。您是否有一些指向示例或描述该示例的文档?我找到了pauldone.blogspot.fr/2014/03/mongoparallelaggregation.html,但它并不完全相同。
  • @SylvainLeroux 很乐意,如果你能等一天左右,我可能会解决它,或者其他人可能会。注意到已经有一个提交的答案在这里使用了“A/B”变体,我称之为对我之前提到的初始“愚蠢”响应的修订,这在一定程度上缓解了“填充数组”问题,但也不是很好高性能..您的实际语言实现应该是一种可以发出“并行”操作并努力组合结果的语言。 Node.js async 是一个模板示例。

标签: mongodb aggregation-framework


【解决方案1】:

这几乎应该是一个 MongoDB 常见问题解答,主要是因为它是一个真实的示例概念,说明您应该如何从 SQL 处理改变您的思维并接受像 MongoDB 这样的引擎所做的事情。

这里的基本原则是“MongoDB 不做连接”。任何“构想”如何构建 SQL 来完成此操作的方式本质上都需要“连接”操作。典型的形式是“UNION”,实际上是“join”。

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

艰难的道路

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 不执行“join”,那么如何处理呢?


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

在架构和性能方面,您最好的方法是通过客户端 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 ) );
    }
  );
});

这给出了可爱的结果:

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

所以这里要注意的关键是“单独的”聚合语句本身实际上非常简单。您唯一面临的就是将这些组合到您的最终结果中。有很多方法可以“组合”,特别是处理每个查询的大结果,但这是执行模型的基本示例。


这里是重点。

  • 可以在聚合管道中对数据进行混洗,但对于大型数据集而言性能不佳。

  • 使用支持“并行”和“异步”执行的语言实现和 API,这样您就可以一次“加载”所有或“大部分”操作。

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

  • 忘记 SQL 方式。 NoSQL 方式将诸如“连接”之类的事情的处理委托给您的“数据逻辑层”,它包含此处显示的代码。这样做是因为它可以扩展到非常大的数据集。在大型应用程序中,您的“数据逻辑”处理节点的工作是将其传递到最终 API。

与我可能描述的任何其他形式的“争吵”相比,这。 “NoSQL”思维的一部分是“忘掉你所学的”并以不同的方式看待事物。如果这种方式不能更好地执行,那么请坚持使用 SQL 方法进行存储和查询。

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

【讨论】:

  • 很好的答案!但是,有一个问题:如果集合同时更新怎么办——我们最终会不会让两个查询返回不一致的结果?
  • @SylvainLeroux 如果集合实际上是同时更新的,那么无论采用何种方法,您都可以获得不同的结果。锁定 MongoDB 是一个“嗯”的问题。所以我就这样吧。真正“快照”任何东西是相当困难的,所以这基本上是另一篇非常冗长的博文(或系列)。
【解决方案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 字段。

噗!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-23
    • 2021-06-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多