从mongodb中的子文档数组中拉出(除了一个)文档

时间:2014-06-04 22:49:24

标签: javascript mongodb mongodb-query aggregation-framework

我有一组我刚刚导入的数据。在每次导入时,我都会将“历史”子文档附加到历史数组中。整体结构是这样的,但有更多的领域:

{ _id: ObjectId('000000000000000001'),
  history: [ {date: ISODate("2014-05-25T22:00:00Z"), value: 1},
             {date: ISODate("2014-05-26T22:00:00Z"), value: 1},
             {date: ISODate("2014-05-26T22:00:00Z"), value: 1} 
  ]
}

问题是,在一些情况下,导入很糟糕,我最终在同一天重复了历史记录。我想删除所有重复项。我尝试使用$pull更新运算符执行此操作,并将重复调用它,直到每个日期都有适当数量的历史记录条目。问题是,我有超过一百万个数据点,每个数据点都有不同数量的重复数据 - 有些数据点多达12个。有没有办法在没有使用mapReduce的情况下拉除除一个之外的所有东西?我想的是:

db.test.update({'history.date': new Date(2014,4,26)},
               {
                $pullAll : 
                   {'history': {date: new Date(2014,4,27)}},
                $push : {'history' : {}}
               }, {multi:true})

3 个答案:

答案 0 :(得分:2)

试试这个,效果很好:

db.collection.find().forEach(function(doc) {
     db.collection.update(
         { "_id": doc._id },
         { "$set": { "history": [doc.history] } }
     );
})

答案 1 :(得分:1)

你提出的建议的问题是你实际上在你的陈述中最终会有冲突的路径,因为这两个操作都在"历史"阵列。所以这些操作实际上并没有按顺序执行#34;正如您可能认为的那样,这会导致冲突,在尝试解析查询时会产生错误。

你基本上也是"擦拭"数组的内容,如果你的符号只是真正的简写而不是打算只是推动"和空对象{},然后根据该文档中找到的现有值,确实无法更新文档。

所以最后的方法是循环来做这件事,这真的不是那么糟糕:

 db.collection.find().forEach(function(doc) {
     db.collection.update(
         { "_id": doc._id },
         { "$set": { "history": [] } }
     );
     db.collection.update(
         { "_id": doc._id },
         { "$addToSet": { "history": { "$each": doc.history } } }
     );
 })

当然,如果您拥有MongoDB 2.6或更高版本,您可以在批量操作中实现这一目标,从而提高效率:

 var count = 0;
 var bulk = db.collection.initializeOrderedBulkOp();

 db.collection.find().forEach(function(doc) {

    bulk.find({ "_id": doc._id }).update({
        "$set": { "history": [] }
    });
    bulk.find({ "_id": doc._id }).update({
        "$addToSet": { "history": { "$each": doc.history } }
    });
    count++;

    if ( count % 500 == 0 ) {
        bulk.execute();
        bulk = db.collection.initializeOrderedBulkOp();
        count = 0;
    }

 });

 if ( count > 0 )
     bulk.execute();

因此,对操作组合并发送500或1000组操作,这些操作应安全地遵循BSON 16MB限制,当然您可以根据需要进行调整。虽然每个更新实际上是按顺序执行的,但在此示例中,每500个项目仅对服务器发送一次实际发送/响应。

您也可以考虑使用聚合方法查找包含重复项的文档,以便通过不更新不需要更新的文档来提高效率:

db.collection.aggregate([
    { "$project": {
       "_id": "$$ROOT",
       "history": 1
    }},
    { "$unwind": "$history" },
    { "$group": {
        "_id": { "date": "$history.date", "value": "$history.value" },
        "orig": { "$first": "_id" }
    }},
    { "$group": {
        "_id": "$orig._id",
        "history": { "$first": "$orig.history" }
    }}
]).forEach(function(doc) {
    // same as above

甚至可以将其用作跳板来删除重复内容,这样您只需要使用$set每个循环发送一个更新,方法是删除重复项

 var count = 0;
 var bulk = db.collection.initializeOrderedBulkOp();

db.collection.aggregate([
    { "$unwind": "$history" },
    { "$group": {
        "_id": { "date": "$history.date", "value": "$history.value" },
        "orig": { "$first": "_id" }
    }},
    { "$group": {
        "_id": "$orig._id",
        "history": { "$push": "$_id" }
    }}
]).forEach(function(doc) {

    bulk.find({ "_id": doc._id }).update({
        "$set": { "history": doc.history }
    });
    count++;

    if ( count % 500 == 0 ) {
        bulk.execute();
        bulk = db.collection.initializeOrderedBulkOp();
        count = 0;
    }
]);

 if ( count > 0 )
     bulk.execute();

因此,有一些方法可以摆脱那些您可以考虑并适应您需求的重复条目。

答案 2 :(得分:0)

当我想到我可以在mongo shell中分三步执行此操作时,我正准备实现上面提到的一个脚本:

date = new Date(2014,4,26);
temp = 'SOMESPECIALTEMPVALUE'

db.test.update({'history.date': date},
           {$set: {
               'history.$.date' : temp
           }}, {multi:true})

db.test.update({'history.date': temp},
           {$pull: {
               'history.date' : temp
           }}, {multi:true})   

db.test.update({'history.date': temp},
           {$set: {
               'history.$.date' : date
           }}, {multi:true})

这是有效的,因为$仅更新第一个匹配的子文档。使用pull我然后删除所有剩余的重复项。最后,我将剩余的临时值重置为其原始值。这对我来说效果很好,因为它是一次只有三个主观日期的操作。否则我可能会采用脚本方法。