【问题标题】:Count how many documents contain a field计算有多少文档包含一个字段
【发布时间】:2016-04-16 13:06:54
【问题描述】:

我有这三个 MongoDB 文档:

{ 
    "_id" : ObjectId("571094afc2bcfe430ddd0815"), 
    "name" : "Barry", 
    "surname" : "Allen", 
    "address" : [
        {
            "street" : "Red", 
            "number" : NumberInt(66), 
            "city" : "Central City"
        }, 
        {
            "street" : "Yellow", 
            "number" : NumberInt(7), 
            "city" : "Gotham City"
        }
    ]
}

{ 
    "_id" : ObjectId("57109504c2bcfe430ddd0816"), 
    "name" : "Oliver", 
    "surname" : "Queen", 
    "address" : {
        "street" : "Green", 
        "number" : NumberInt(66), 
        "city" : "Star City"
    }
}
{ 
    "_id" : ObjectId("5710953ac2bcfe430ddd0817"), 
    "name" : "Tudof", 
    "surname" : "Unknown", 
    "address" : "homeless"
}

address 字段是第一个文档中的对象的 Array,第二个文档中的 Object 和第三个文档中的 String。 我的目标是找出我的集合中有多少文档包含字段 address.street。在这种情况下,正确的计数是 1,但通过我的查询,我得到了两个:

db.coll.find({"address.street":{"$exists":1}}).count()

我也尝试过 map/reduce。它有效,但速度较慢;所以如果可能的话,我会避免它。

【问题讨论】:

    标签: mongodb mongodb-query aggregation-framework


    【解决方案1】:

    这里的区别在于.count() 操作在返回字段存在的“文档”计数时实际上是“正确的”。因此,一般考虑可分解为:

    如果你只想排除带有数组字段的文档

    然后最有效的方法是排除那些“街道”是“地址”属性的文档作为“数组”,然后只需使用点符号属性查找0索引不存在排除在外:

    db.coll.find({
      "address.street": { "$exists": true },
      "address.0": { "$exists": false }
    }).count()
    

    作为本机编码的运算符测试,$exists 在这两种情况下都能有效地完成正确的工作。

    如果您打算计算字段出现次数

    如果您实际询问的是“字段计数”,其中一些“文档”包含数组条目,其中该“字段”可能出现多次。

    为此,您需要您提到的聚合框架或 mapReduce。 MapReduce 使用基于 JavaScript 的处理,因此将比.count() 操作慢得多。聚合框架还需要计算并且“将”比.count() 慢,但不会像 mapReduce 那么多。

    在 MongoDB 3.2 中,您可以通过 $sum 处理值数组以及作为分组累加器的扩展能力获得一些帮助。这里的另一个助手是$isArray,当数据实际上是“数组”时,它允许通过$map 使用不同的处理方法:

    db.coll.aggregate([
      { "$group": {
        "_id": null,
        "count": {
          "$sum": {
            "$sum": {
              "$cond": {
                "if": { "$isArray": "$address" },
                "then": {
                  "$map": {
                    "input": "$address",
                    "as": "el",
                    "in": {
                      "$cond": {
                        "if": { "$ifNull": [ "$$el.street", false ] },
                        "then": 1,
                        "else": 0
                      }
                    }
                  }
                },
                "else": {
                  "$cond": {
                    "if": { "$ifNull": [ "$address.street", false ] },
                    "then": 1,
                    "else": 0
                  }
                }
              }
            }
          }
        }
      }}
    ])
    

    早期版本依赖于更多的条件处理,以便以不同方式处理数组和非数组数据,并且通常需要$unwind 来处理数组条目。

    在 MongoDB 2.6 中通过 $map 转置数组:

    db.coll.aggregate([
      { "$project": {
        "address": {
          "$cond": {
            "if": { "$ifNull": [ "$address.0", false ] },
            "then": "$address",
            "else": {
              "$map": {
                "input": ["A"],
                "as": "el",
                "in": "$address"
              }
            }
          }
        }
      }},
      { "$unwind": "$address" },
      { "$group": {
        "_id": null,
        "count": {
          "$sum": {
            "$cond": {
              "if": { "$ifNull": [ "$address.street", false ] },
              "then": 1,
              "else": 0
            }
          }
        }
      }}
    ])
    

    或者使用 MongoDB 2.2 或 2.4 提供条件选择:

    db.coll.aggregate([
      { "$group": {
        "_id": "$_id",
        "address": { 
          "$first": {
            "$cond": [
              { "$ifNull": [ "$address.0", false ] },
              "$address",
              { "$const": [null] }
            ]
          }
        },
        "other": {
          "$push": {
            "$cond": [
              { "$ifNull": [ "$address.0", false ] },
              null,
              "$address"
            ]
          }
        },
        "has": { 
          "$first": {
            "$cond": [
              { "$ifNull": [ "$address.0", false ] },
              1,
              0
            ]
          }
        }
      }},
      { "$unwind": "$address" },
      { "$unwind": "$other" },
      { "$group": {
        "_id": null,
        "count": {
          "$sum": {
            "$cond": [
              { "$eq": [ "$has", 1 ] },
              { "$cond": [
                { "$ifNull": [ "$address.street", false ] },
                1,
                0
              ]},
              { "$cond": [
                { "$ifNull": [ "$other.street", false ] },
                1,
                0
              ]}
            ]
          }
        }
      }}
    ])
    

    所以后一种形式“应该”比 mapReduce 表现好一点,但可能不会好很多。

    在所有情况下,逻辑都归结为使用 $ifNull 作为聚合框架的 $exists 的“逻辑”形式。与$cond 配对,当属性实际存在时获得“真实”结果,不存在时返回false 值。这决定了是1还是0分别通过$sum返回到整体累加中。

    理想情况下,您拥有可以在单个 $group 管道阶段执行此操作的现代版本,否则您需要更长的路径。

    【讨论】:

    • 这是一个很好的答案。我的目标是查找集合中某个字段的出现次数。起初我使用 count() 但效果不太好。现在我使用递归映射减少来计算出现次数。它有效,但速度很慢。我会试试你的建议,谢谢。
    • 完美运行。但是假设我使用另一个递归 map reduce 来查找我的集合中存在的所有字段。对于每个字段,我必须计算出现次数。如果我不知道模式的形状,是否可以使用聚合来计算出现次数,还是必须使用 map reduce?
    • @DistribuzioneGaussiana 如果您还有其他问题,那么Ask Another Question,因为我们只能真正回答您直接提出的问题,最好在自己的帖子中表达另一个问题,您可以在其中放置所有细节。您在此处实际提出的问题显示了具有一致路径的文档结构,更不用说只有您的原始问题标题才建议您实际上在哪里尝试计算字段出现次数,因为问题正文的其余部分涉及匹配文档。
    【解决方案2】:

    你可以试试这个:

    db.getCollection('collection_name').find({
            "address.street":{"$exists":1},
            "$where": "Array.isArray(this.address) == false && typeof this.address === 'object'"
    });
    

    在 where 子句中,我们排除了如果地址是数组并且 如果它的类型是对象,则包括地址。

    【讨论】:

    • 虽然问题标题有些误导,但 OP 确实明确表示他们希望从他们的数据中计数“三”。这确实表明包含具有“两个”匹配项的“数组”的文档将包含在总数中。因此,数组文档中的“二”,具有单个对象的文档中的“一”,“街道”不是“地址”子属性的文档中的“无”。这不是您在此处提交的查询所做的。
    • 其实我觉得我一开始看错了,但还是有比使用$where 快得多的方法。只需在条件中使用"address.0": { "$exists": false }。由于0 索引仅在字段实际上是“数组”时才存在。
    猜你喜欢
    • 1970-01-01
    • 2023-02-21
    • 2023-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多