聚合子文档忽略键顺序

时间:2017-08-10 17:55:27

标签: mongodb mongodb-query aggregation-framework

测试数据:

    db.moretest.insert(
[
{ "title" : { "a" : 1, "b" : 2 } },
{ "title" : { "a" : 1, "b" : 2 } },
{ "title" : { "b" : 2, "a" : 1 } },
{ "title" : { "foo" : 42, "a" : 1 } },
]
)

我想计算一个键在“标题”中出现的频率,忽略顺序。例如。 { "a" : 1, "b" : 2 }{ "b" : 2, "a" : 1 }应该被视为相同。

但是,此查询不会产生所需的结果:

db.moretest.aggregate(
   [
     { $group: { "_id": "$title", "count": { $sum: 1 } } }
   ]
);

结果

{ "_id" : { "foo" : 42, "a" : 1 }, "count" : 1 }
{ "_id" : { "b" : 2, "a" : 1 }, "count" : 1 }
{ "_id" : { "a" : 1, "b" : 2 }, "count" : 2 }

但我想要的是以下内容:

{ "_id" : { "foo" : 42, "a" : 1 }, "count" : 1 }
{ "_id" : { "a" : 1, "b" : 2 }, "count" : 3 }

1 个答案:

答案 0 :(得分:1)

MongoDB实际上确实考虑了对象键中的这种不同顺序来表示"唯一性"。对于一般"查询"目的,这就是"dot notation"表单存在的原因,指定path to keys at "depth"而不是完全匹配格式。

出于同样的原因,这也适用于聚合。如果你想以任何顺序组合,那么你实际需要强迫订单"保持一致。

这在现代版since MongoDB 3.4.4中完成:

db.moretest.aggregate([
  { "$project": {
    "title": { "$objectToArray": "$title" },
  }},
  { "$unwind": "$title" },
  { "$sort": { "_id": 1, "title.k": 1 } },
  { "$group": {
    "_id": "$_id",
    "title": { "$push": "$title" }    
  }},
  { "$group": {
    "_id": { "$arrayToObject": "$title" },
    "count": { "$sum": 1 }  
  }}
])

可以使用$objectToArray来转换"键"进入"数组"然后可以"排序"。问题是,为了做到这一点,您仍然需要$unwind数组元素并应用$sort管道阶段然后$group返回到数组中,然后再转换回{{3} }。

但确实得到了结果:

/* 1 */
{
    "_id" : {
        "a" : 1.0,
        "b" : 2.0
    },
    "count" : 3.0
}

/* 2 */
{
    "_id" : {
        "a" : 1.0,
        "foo" : 42.0
    },
    "count" : 1.0
}

即使不是很有效率。因此,能够对阵列进行排序会更好。

你"可以"通过测试"特定键"来交替决定呈现"title"的方式,尽管是以一种非常黑客的方式:

db.moretest.aggregate([
  { "$group": {
    "_id": {
      "$cond": {
        "if": { "$ifNull": [ "$title.b", false ] },
        "then": { "a": "$title.a", "b": "$title.b" },
        "else": "$title"
      }
    },
    "count": { "$sum": 1 }  
  }}
])

这是相同的,当然实际上会重新排序"任何不符合提供条件的物体的钥匙。然而,它需要预先知道目标对象中的键实际上是为了提供条件。但如果您的实际用例支持实用,那么它可能是一个可行的选择。

对于其他版本并且更有效(即使依赖JavaScript解释来执行此操作)正在使用.mapReduce()

db.moretest.mapReduce(
  function() {
    emit(
      Object.keys(this.title).sort()
        .reduce((acc,curr) => Object.assign(acc,{ [curr]: this.title[curr] }), {}),
      1
    );
  },
  function(key,values) { return Array.sum(values) },
  { "out": { "inline": 1 } }
)

这或多或少都是相同的,但它有自己的结果集格式:

"results" : [ 
    {
        "_id" : {
            "a" : 1.0,
            "b" : 2.0
        },
        "value" : 3.0
    }, 
    {
        "_id" : {
            "a" : 1.0,
            "foo" : 42.0
        },
        "value" : 1.0
    }
],