【问题标题】:MongoDB lists - get every Nth itemMongoDB 列表 - 获取每第 N 个项目
【发布时间】:2015-09-22 18:35:55
【问题描述】:

我有一个大致如下的 Mongodb 架构:

[
  {
    "name" : "name1",
    "instances" : [ 
      {
        "value" : 1,
        "date" : ISODate("2015-03-04T00:00:00.000Z")            
      }, 
      {
        "value" : 2,
        "date" : ISODate("2015-04-01T00:00:00.000Z")
      }, 
      {
        "value" : 2.5,
        "date" : ISODate("2015-03-05T00:00:00.000Z")
      },
      ...
    ]
  },
  {
    "name" : "name2",
    "instances" : [ 
      ...
    ]
  }
]

每个元素的实例数量可能非常大。

我有时只想获取数据的一个样本,也就是说,每 3 个实例或每 10 个实例获取一次……你懂的。

我可以通过获取所有实例并在我的服务器代码中过滤它们来实现这个目标,但我想知道是否有办法通过使用一些聚合查询来实现。

有什么想法吗?


更新

假设数据结构如@SylvainLeroux 建议的那样平坦,即:

[
  {"name": "name1", "value": 1, "date": ISODate("2015-03-04T00:00:00.000Z")},
  {"name": "name2", "value": 5, "date": ISODate("2015-04-04T00:00:00.000Z")},
  {"name": "name1", "value": 2, "date": ISODate("2015-04-01T00:00:00.000Z")},
  {"name": "name1", "value": 2.5, "date": ISODate("2015-03-05T00:00:00.000Z")},
  ...
]

获取每第 N 个项目(特定name)的任务会更容易吗?

【问题讨论】:

  • FWIW: “每个元素的实例数量可能会很大。” 如果文档数量如此之多,这里使用嵌入式文档可能不是一个好主意大到它最终可以达到 32MB 的限制。此外,如果您经常需要访问该“大”数组的子集,则可能是该模式不合适的另一个线索。在不知道您的确切需求的情况下,我会想到更像 { "name" : "name1", value: 1, date: ... }, { "name" : "name1", value: 2, date: ... }, ... 的东西。使用该架构,您甚至可以(ab)使用 _id 字段来选择一个有偏见的数据样本。
  • @SylvainLeroux,实例共享的不仅仅是一个名称(为了这个问题的目的,我简化了架构)。我不希望每个实例都包含与其他实例基本相同的数据,因此我有一个包含多个实例的父对象。这样对我来说更有意义。但我愿意接受建议......

标签: mongodb mongodb-query aggregation-framework


【解决方案1】:

不幸的是,使用聚合框架是不可能的,因为这需要带有 $unwind 的选项来发出数组索引/位置,而目前聚合无法处理这些索引/位置。 SERVER-4588 有一个开放的 JIRA 票证。

但是,一种解决方法是使用 MapReduce 但这会带来巨大的性能成本,因为获取数组索引的实际计算是使用嵌入式 JavaScript 引擎执行的(速度很慢) ,并且仍然有一个全局 JavaScript 锁,它只允许单个 JavaScript 线程一次运行。

使用 mapReduce,您可以尝试以下操作:

映射函数:

var map = function(){
    for(var i=0; i < this.instances.length; i++){
        emit(
            { "_id": this._id,  "index": i },
            { "index": i, "value": this.instances[i] }
        );
    }
};

减少功能:

var reduce = function(){}

然后您可以在您的收藏集上运行以下 mapReduce 函数:

db.collection.mapReduce( map, reduce, { out : "resultCollection" } );

然后您可以使用 map() 游标方法查询结果集合以获取实例数组中每第 N 项的列表/数组:

var thirdInstances = db.resultCollection.find({"_id.index": N})
                                        .map(function(doc){return doc.value.value})

【讨论】:

  • 如您所说,使用 MapReduce 进行即席查询会有一些性能损失。但是,根据用例,如果可以接受使用陈旧数据,则 OP 可以安排 MapReduce 作业定期运行,以便处理一些他可以稍后查询的聚合集合。
  • @SylvainLeroux 好电话,绝对同意。
  • JIRA 问题现已关闭并修复。
【解决方案2】:

您的问题似乎明确提出了“获取每 n 个实例”,这似乎是一个非常明确的问题。

.find() 这样的查询操作实际上只能“按原样”返回文档,除了投影中的通用字段“选择”和允许单个匹配数组的positional $ 匹配运算符或$elemMatch 等运算符元素。

当然有$slice,但它只允许对数组进行“范围选择”,所以同样不适用。

可以在服务器上修改结果的“唯一”事物是.aggregate().mapReduce()。前者不会以任何方式“很好地”“切片”数组,至少不是“n”个元素。然而,由于 mapReduce 的“function()”参数是基于 JavaScript 的逻辑,所以你有更多的空间来玩。

对于分析过程和“仅”用于分析目的,只需使用 .filter() 通过 mapReduce 过滤数组内容:

db.collection.mapReduce(
    function() {
        var id = this._id;
        delete this._id;

        // filter the content of "instances" to every 3rd item only
        this.instances = this.instances.filter(function(el,idx) {
            return ((idx+1) % 3) == 0;
        });
        emit(id,this);
    },
    function() {},
    { "out": { "inline": 1 } } // or output to collection as required
)

此时它实际上只是一个“JavaScript 运行程序”,但如果这只是用于分析/测试,那么这个概念通常没有任何问题。当然,输出并不是“确切”文档的结构,但它与 mapReduce 所能获得的一样接近传真。

我在这里看到的另一个建议是创建一个包含所有“非规范化”项的新集合,并将数组中的“索引”作为唯一 _id 键的一部分插入。这可能会产生一些你可以直接查询的东西,但对于你仍然需要做的“每 n 个项目”:

db.resultCollection.find({
     "_id.index": { "$in": [2,5,8,11,14] } // and so on ....
})

因此,计算并提供“每第 n 项”的索引值,以获得“每第 n 项”。所以这似乎并不能真正解决所提出的问题。

如果输出形式似乎更适合您的“测试”目的,那么对这些结果更好的后续查询将使用聚合管道,$redact

db.newCollection([
    { "$redact": {
        "$cond": {
            "if": {
                "$eq": [ 
                    { "$mod": [ { "$add": [ "$_id.index", 1] }, 3 ] },
                0 ]
            },
            "then": "$$KEEP",
            "else": "$$PRUNE"
        }
    }}
])

这至少使用了与之前.filter() 应用的“逻辑条件”非常相似,只选择“第 n 个索引”项,而不列出所有可能的索引值作为查询参数。

【讨论】:

    【解决方案3】:

    或者只有一个查找块:

    db.Collection.find({}).then(function(data) {
      var ret = [];
      for (var i = 0, len = data.length; i < len; i++) {
        if (i % 3 === 0 ) {
          ret.push(data[i]);
        }
      }
      return ret;
    });
    

    返回一个 promise,您可以调用它的 then() 来获取第 N 个模数数据。

    【讨论】:

    • 正如您在我的问题中看到的那样,我已经说过我可以获取所有项目并在我的代码中过滤它们。我的问题是是否有办法通过 mongo 查询来做到这一点。
    【解决方案4】:

    这里不需要$unwind。您可以使用$push$arrayElemAt 将数组值投影到$group 聚合内的请求索引处。

    类似

    db.colname.aggregate(
    [
      {"$group":{
        "_id":null,
        "valuesatNthindex":{"$push":{"$arrayElemAt":["$instances",N]}
        }}
      },
      {"$project":{"valuesatNthindex":1}}
    ])
    

    【讨论】:

    • 此命令是否需要将整个集合加载到内存中?
    • @GabrielFair 聚合在服务器上运行,因此如果您要求的是,客户端不会加载任何内容。
    • 我想他是在问是否需要将完整的集合加载到服务器的内存中。
    【解决方案5】:

    您可以使用以下聚合:

    db.col.aggregate([
        {
            $project: {
                instances: {
                    $map: {
                        input: { $range: [ 0, { $size: "$instances" }, N ] },
                        as: "index",
                        in: { $arrayElemAt: [ "$instances", "$$index" ] }
                    }
                }
            }
        }
    ])
    

    $range 生成索引列表。第三个参数代表非零步长。对于N = 2,它将是[0,2,4,6...],对于N = 3,它将返回[0,3,6,9...],依此类推。然后你可以使用$mapinstances数组中获取对应的项目。

    【讨论】:

      【解决方案6】:

      您可能会喜欢这种使用 $lookup 聚合的方法。并且可能是最方便、最快捷的方式,无需任何聚合技巧。

      使用以下架构创建集合Names

      [
        { "_id": 1, "name": "name1" },
        { "_id": 2, "name": "name2" }
      ]
      

      然后是 Instances 集合,其父 ID 为 "nameId"

      [
        { "nameId": 1, "value" : 1, "date" : ISODate("2015-03-04T00:00:00.000Z") },
        { "nameId": 1, "value" : 2, "date" : ISODate("2015-04-01T00:00:00.000Z") },
        { "nameId": 1, "value" : 3, "date" : ISODate("2015-03-05T00:00:00.000Z") },
        { "nameId": 2, "value" : 7, "date" : ISODate("2015-03-04T00:00:00.000Z") }, 
        { "nameId": 2, "value" : 8, "date" : ISODate("2015-04-01T00:00:00.000Z") }, 
        { "nameId": 2, "value" : 4, "date" : ISODate("2015-03-05T00:00:00.000Z") }
      ]
      

      现在使用$lookup 聚合3.6 语法,您可以在$lookup pipeline 内使用$sample 来随机获取每个Nth 元素。

      db.Names.aggregate([
        { "$lookup": {
          "from": Instances.collection.name,
          "let": { "nameId": "$_id" },
          "pipeline": [
            { "$match": { "$expr": { "$eq": ["$nameId", "$$nameId"] }}},
            { "$sample": { "size": N }}
          ],
          "as": "instances"
        }}
      ])
      

      你可以测试一下here

      【讨论】:

      • $sample - 从其输入中随机选择指定数量的文档。我不认为这个问题是关于随机抽样的。
      • YvanPearson 如果 OP 需要数组中的特定元素,那么其他聚合运算符如 $arrayElemAt$slice 将在这里工作。但这不是这里的要求。 OP 需要随机元素。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-07-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-27
      相关资源
      最近更新 更多