在一个查询中为每个属性分组不同的值和计数

时间:2017-11-15 08:17:41

标签: javascript mongodb mongodb-query aggregation-framework

我在个人资料集合中有数据

[
    {
        name: "Harish",
        gender: "Male",
        caste: "Vokkaliga",
        education: "B.E"
    },
    {
        name: "Reshma",
        gender: "Female",
        caste: "Vokkaliga",
        education: "B.E"
    },
    {
        name: "Rangnath",
        gender: "Male",
        caste: "Lingayath",
        education: "M.C.A"
    },
    {
        name: "Lakshman",
        gender: "Male",
        caste: "Lingayath",
        education: "B.Com"
    },
    {
        name: "Reshma",
        gender: "Female",
        caste: "Lingayath",
        education: "B.E"
    }
]

这里我需要计算不同性别的总数,不同种姓的总数和不同教育的总数。 预期o / p

{
    gender: [{
        name: "Male",
        total: "3"
    },
    {
        name: "Female",
        total: "2"
    }],
    caste: [{
        name: "Vokkaliga",
        total: "2"
    },
    {
        name: "Lingayath",
        total: "3"
    }],
    education: [{
        name: "B.E",
        total: "3"
    },
    {
        name: "M.C.A",
        total: "1"
    },
    {
        name: "B.Com",
        total: "1"
    }]
}

使用mongodb聚合如何获得预期的结果。

1 个答案:

答案 0 :(得分:3)

根据可用的版本,有不同的方法,但它们都基本上分解为将文档字段转换为"数组中的单独文档"然后"展开"该数组包含$unwind并执行连续$group个阶段以累积输出总数和数组。

MongoDB 3.4.4及以上

最新版本包含$arrayToObject$objectToArray等特殊运营商,可以转移到最初的"数组"源文档比早期版本更具动态性:

db.profile.aggregate([
  { "$project": { 
     "_id": 0,
     "data": { 
       "$filter": {
         "input": { "$objectToArray": "$$ROOT" },
         "cond": { "$in": [ "$$this.k", ["gender","caste","education"] ] }
       }   
     }
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": "$data",
    "total": { "$sum": 1 }  
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": "$data"
    }
  }}
])

因此,使用$objectToArray,您可以将初始文档放入其数组键和值中,作为生成的对象数组中的"k""v"键。我们在此处应用$filter,以便通过" key"进行选择。这里使用$in和我们想要的密钥列表,但这可以更动态地用作"排除"的密钥列表。哪个更短。它只是使用逻辑运算符来评估条件。

此处的结束阶段使用$replaceRoot,因为我们所有的操作和"分组"在两者之间仍然保留"k""v"形式,然后我们在这里使用$arrayToObject来宣传我们的"对象数组"结果是"键"输出中的顶级文档。

MongoDB 3.6 $ mergeObjects

作为额外的皱纹,MongoDB 3.6包括$mergeObjects,它也可以在"accumulator"管道阶段用作$group,从而取代$push并制作最后的$replaceRoot只是将"data"键移动到" root"而是返回的文件:

db.profile.aggregate([
  { "$project": { 
     "_id": 0,
     "data": { 
       "$filter": {
         "input": { "$objectToArray": "$$ROOT" },
         "cond": { "$in": [ "$$this.k", ["gender","caste","education"] ] }
       }   
     }
  }},
  { "$unwind": "$data" },
  { "$group": { "_id": "$data", "total": { "$sum": 1 } }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": {
      "$mergeObjects": {
        "$arrayToObject": [
          [{ "k": "$_id", "v": "$v" }]
        ] 
      }
    }  
  }},
  { "$replaceRoot": { "newRoot": "$data"  } }
])

这与整体演示的内容并没有什么不同,只是简单地演示了$mergeObjects如何以这种方式使用,并且可能在分组键不同而且我们不希望这样的情况下有用最终"合并"到对象的根空间。

请注意,仍需要$arrayToObject来转换"值"回到"键"的名称,但我们只是在积累期间而不是在分组之后进行,因为新的累积允许"合并"钥匙。

MongoDB 3.2

将它取回一个版本,或者即使你的MongoDB 3.4.x小于3.4.4版本,我们仍然可以使用大部分内容,而是以更静态的方式处理阵列的创建,以及处理最终的"转换"由于我们没有的聚合运算符,输出的输出方式不同:

db.profile.aggregate([
  { "$project": {
    "data": [
      { "k": "gender", "v": "$gender" },
      { "k": "caste", "v": "$caste" },
      { "k": "education", "v": "$education" }
    ]
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": "$data",
    "total": { "$sum": 1 }  
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  /*
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": "$data"
    }
  }}
  */
]).map( d => 
  d.data.map( e => ({ [e.k]: e.v }) )
    .reduce((acc,curr) => Object.assign(acc,curr),{})
)

这是完全相同的事情,除了没有将文档动态转换为数组,我们实际上"明确"为每个数组成员分配相同的"k""v"表示法。实际上,此时只保留常规的关键名称,因为这里的聚合运算符都不依赖于此。

同样不是使用$replaceRoot,我们只是做了与之前的管道阶段实现相同的事情,而是在客户端代码中。所有MongoDB驱动程序都有一些cursor.map()的实现,以启用"游标转换"。在shell中,我们使用Array.map()Array.reduce()的基本JavaScript函数来获取该输出,并再次将数组内容提升为返回的顶级文档的键。

MongoDB 2.6

回到MongoDB 2.6以涵盖其间的版本,这里唯一改变的是使用$map$literal来输入数组声明:

db.profile.aggregate([
  { "$project": {
    "data": {
      "$map": {
        "input": { "$literal": ["gender","caste", "education"] },
        "as": "k",
        "in": {
          "k": "$$k",
          "v": {
            "$cond": {
              "if": { "$eq": [ "$$k", "gender" ] },
              "then": "$gender",
              "else": {
                "$cond": {
                  "if": { "$eq": [ "$$k", "caste" ] },
                  "then": "$caste",
                  "else": "$education"
                }
              }    
            }
          }    
        }
      }
    }
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": "$data",
    "total": { "$sum": 1 }  
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  /*
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": "$data"
    }
  }}
  */
])
.map( d => 
  d.data.map( e => ({ [e.k]: e.v }) )
    .reduce((acc,curr) => Object.assign(acc,curr),{})
)

因为这里的基本想法是"迭代"提供的字段名称数组,值的实际分配来自"嵌套" $cond语句。对于三种可能的结果,这意味着只有一个嵌套,以便分支"分支"对于每个结果。

来自3.4的现代MongoDB具有$switch,这使得这种分支更简单,但这表明逻辑总是可行的,并且自从MongoDB 2.2中引入聚合框架以来$cond运算符已经存在。

同样,对游标结果进行相同的转换也适用,因为那里没有新的东西,并且大多数编程语言都有能力这么做多年,如果不是从初始开始。

当然,基本过程甚至可以回到MongoDB 2.2,但只是以不同的方式应用数组创建和$unwind。但是目前没有人应该运行2.8以下的任何MongoDB,甚至从3.0开始的官方支持甚至还在快速耗尽。

输出

对于可视化,此处所有演示的管道的输出在最后一次"转换之前具有以下形式。完了:

/* 1 */
{
    "_id" : null,
    "data" : [ 
        {
            "k" : "gender",
            "v" : [ 
                {
                    "name" : "Male",
                    "total" : 3.0
                }, 
                {
                    "name" : "Female",
                    "total" : 2.0
                }
            ]
        }, 
        {
            "k" : "education",
            "v" : [ 
                {
                    "name" : "M.C.A",
                    "total" : 1.0
                }, 
                {
                    "name" : "B.E",
                    "total" : 3.0
                }, 
                {
                    "name" : "B.Com",
                    "total" : 1.0
                }
            ]
        }, 
        {
            "k" : "caste",
            "v" : [ 
                {
                    "name" : "Lingayath",
                    "total" : 3.0
                }, 
                {
                    "name" : "Vokkaliga",
                    "total" : 2.0
                }
            ]
        }
    ]
}

然后通过$replaceRoot或光标变换显示结果变为:

/* 1 */
{
    "gender" : [ 
        {
            "name" : "Male",
            "total" : 3.0
        }, 
        {
            "name" : "Female",
            "total" : 2.0
        }
    ],
    "education" : [ 
        {
            "name" : "M.C.A",
            "total" : 1.0
        }, 
        {
            "name" : "B.E",
            "total" : 3.0
        }, 
        {
            "name" : "B.Com",
            "total" : 1.0
        }
    ],
    "caste" : [ 
        {
            "name" : "Lingayath",
            "total" : 3.0
        }, 
        {
            "name" : "Vokkaliga",
            "total" : 2.0
        }
    ]
}

因此,虽然我们可以将一些新的和花哨的运算符放入我们有可用的聚合管道中,但最常见的用例是在这些管道转换的末端"在这种情况下,我们也可以简单地对返回的游标结果中的每个文档执行相同的转换。