MongoDB的$ in子句是否保证订单

时间:2014-04-01 22:01:50

标签: mongodb mongoose mapreduce mongodb-query aggregation-framework

使用MongoDB的$in子句时,返回文档的顺序是否始终对应于数组参数的顺序?

11 个答案:

答案 0 :(得分:68)

如上所述,$ in子句数组中参数的顺序并不反映文档的检索顺序。当然,这将是自然顺序或所选的索引顺序。

如果您需要保留此订单,那么您基本上有两种选择。

因此,假设您在文档中使用_id的值与一个数组进行匹配,该数组将作为$in传递给[ 4, 2, 8 ]

使用Aggregate

的方法
var list = [ 4, 2, 8 ];

db.collection.aggregate([

    // Match the selected documents by "_id"
    { "$match": {
        "_id": { "$in": [ 4, 2, 8 ] },
    },

    // Project a "weight" to each document
    { "$project": {
        "weight": { "$cond": [
            { "$eq": [ "$_id", 4  ] },
            1,
            { "$cond": [
                { "$eq": [ "$_id", 2 ] },
                2,
                3
            ]}
        ]}
    }},

    // Sort the results
    { "$sort": { "weight": 1 } }

])

这就是扩展形式。这里基本上发生的是,正如值数组传递给$in一样,您还构造了一个“嵌套”$cond语句来测试值并分配适当的权重。由于“权重”值反映了数组中元素的顺序,因此您可以将该值传递给排序阶段,以便按所需顺序获取结果。

当然,你实际上是在代码中“构建”管道语句,就像这样:

var list = [ 4, 2, 8 ];

var stack = [];

for (var i = list.length - 1; i > 0; i--) {

    var rec = {
        "$cond": [
            { "$eq": [ "$_id", list[i-1] ] },
            i
        ]
    };

    if ( stack.length == 0 ) {
        rec["$cond"].push( i+1 );
    } else {
        var lval = stack.pop();
        rec["$cond"].push( lval );
    }

    stack.push( rec );

}

var pipeline = [
    { "$match": { "_id": { "$in": list } }},
    { "$project": { "weight": stack[0] }},
    { "$sort": { "weight": 1 } }
];

db.collection.aggregate( pipeline );

使用mapReduce

的方法

当然,如果你的敏感度似乎都很大,那么你可以使用mapReduce做同样的事情,它看起来更简单但可能运行得有点慢。

var list = [ 4, 2, 8 ];

db.collection.mapReduce(
    function () {
        var order = inputs.indexOf(this._id);
        emit( order, { doc: this } );
    },
    function() {},
    { 
        "out": { "inline": 1 },
        "query": { "_id": { "$in": list } },
        "scope": { "inputs": list } ,
        "finalize": function (key, value) {
            return value.doc;
        }
    }
)

这基本上依赖于发出的“关键”值在它们在输入数组中出现的“索引顺序”。


因此,这些基本上是您将输入列表的顺序维持到$in条件的方式,其中您已经按照确定的顺序拥有该列表。

答案 1 :(得分:21)

使用聚合查询的另一种方法仅适用于 MongoDB verion> 3.4 -

此功能归于此blog post

按此顺序提取的示例文档 -

var order = [ "David", "Charlie", "Tess" ];

查询 -

var query = [
             {$match: {name: {$in: order}}},
             {$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}},
             {$sort: {"__order": 1}}
            ];

var result = db.users.aggregate(query);

该帖子的另一个引用解释了这些聚合运算符 -

  

" $ addFields"阶段是3.4中的新功能,它允许您进行" $ project"现有文档的新字段,不知道所有其他现有字段。新的" $ indexOfArray" expression返回给定数组中特定元素的位置。

基本上,addToSet运算符会在找到它时向每个文档添加一个新的order字段,此order字段表示我们提供的数组的原始顺序。然后我们简单地根据这个字段对文档进行排序。

答案 2 :(得分:19)

如果您不想使用aggregate,则另一种解决方案是使用find,然后使用array#sort对客户端的文档结果进行排序:

如果$in值是数字等原始类型,您可以使用以下方法:

var ids = [4, 2, 8, 1, 9, 3, 5, 6];
MyModel.find({ _id: { $in: ids } }).exec(function(err, docs) {
    docs.sort(function(a, b) {
        // Sort docs by the order of their _id values in ids.
        return ids.indexOf(a._id) - ids.indexOf(b._id);
    });
});

如果$in值是非基本类型(如ObjectId s),则需要采用另一种方法,因为indexOf在这种情况下通过引用进行比较。

如果您正在使用Node.js 4.x +,则可以使用Array#findIndexObjectID#equals来处理此问题,方法是将sort功能更改为:

docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) - 
                    ids.findIndex(id => b._id.equals(id)));

或者使用任何Node.js版本,使用下划线/ lodash' s findIndex

docs.sort(function (a, b) {
    return _.findIndex(ids, function (id) { return a._id.equals(id); }) -
           _.findIndex(ids, function (id) { return b._id.equals(id); });
});

答案 3 :(得分:4)

JonnyHK的解决方案类似,您可以使用findmap的组合重新排序从客户端Array.prototype.find返回的文档(如果您的客户端使用JavaScript) EcmaScript 2015中的Collection.find({ _id: { $in: idArray } }).toArray(function(err, res) { var orderedResults = idArray.map(function(id) { return res.find(function(document) { return document._id.equals(id); }); }); }); 函数:

idArray

几点说明:

  • 上面的代码使用的是Mongo Node驱动程序,而不是 Mongoose
  • ObjectIdmap
  • 的数组
  • 我还没有测试过这种方法与排序的性能,但是如果你需要操作每个返回的项目(这很常见),你可以在$product = Product::find($id);回调中进行操作来简化你的代码

答案 4 :(得分:2)

始终?决不。顺序始终相同:undefined(可能是存储文档的物理顺序)。除非你对它进行排序。

答案 5 :(得分:2)

我知道这个问题与Mongoose JS框架有关,但duplicated one是通用的,所以我希望在这里发布一个Python(PyMongo)解决方案。

things = list(db.things.find({'_id': {'$in': id_array}}))
things.sort(key=lambda thing: id_array.index(thing['_id']))
# things are now sorted according to id_array order

答案 6 :(得分:1)

我知道这是一个旧线程,但如果您只是返回数组中Id的值,则可能必须选择此语法。因为我似乎无法获得indexOf值以匹配mongo ObjectId格式。

  obj.map = function() {
    for(var i = 0; i < inputs.length; i++){
      if(this._id.equals(inputs[i])) {
        var order = i;
      }
    }
    emit(order, {doc: this});
  };

How to convert mongo ObjectId .toString without including 'ObjectId()' wrapper -- just the Value?

答案 7 :(得分:1)

对于任何新手来说,这里有一个简短而优雅的解决方案,可以在 2021 年的这种情况下使用 MongoDb 3.6(已测试)保留顺序:

  const idList = ['123', '124', '125']
  const out = await db
    .collection('YourCollection')
    .aggregate([
      // Change uuid to your `id` field
      { $match: { uuid: { $in: idList } } },
      {
        $project: {
          uuid: 1,
          date: 1,
          someOtherFieldToPreserve: 1,
          // Addding this new field called index
          index: {
            // If we want index to start from 1, add an dummy value to the beggining of the idList array
            $indexOfArray: [[0, ...idList], '$uuid'],
            // Otherwise if 0,1,2 is fine just use this line
            // $indexOfArray: [idList, '$uuid'],
          },
        },
      },
      // And finally sort the output by our index
      { $sort: { index: 1 } },
    ])

答案 8 :(得分:0)

您可以使用$或子条保证订单。

所以请改用$or: [ _ids.map(_id => ({_id}))]

答案 9 :(得分:0)

这是从Mongo检索结果后的代码解决方案。使用映射存储索引,然后交换值。

catDetails := make([]CategoryDetail, 0)
err = sess.DB(mdb).C("category").
    Find(bson.M{
    "_id":       bson.M{"$in": path},
    "is_active": 1,
    "name":      bson.M{"$ne": ""},
    "url.path":  bson.M{"$exists": true, "$ne": ""},
}).
    Select(
    bson.M{
        "is_active": 1,
        "name":      1,
        "url.path":  1,
    }).All(&catDetails)

if err != nil{
    return 
}
categoryOrderMap := make(map[int]int)

for index, v := range catDetails {
    categoryOrderMap[v.Id] = index
}

counter := 0
for i := 0; counter < len(categoryOrderMap); i++ {
    if catId := int(path[i].(float64)); catId > 0 {
        fmt.Println("cat", catId)
        if swapIndex, exists := categoryOrderMap[catId]; exists {
            if counter != swapIndex {
                catDetails[swapIndex], catDetails[counter] = catDetails[counter], catDetails[swapIndex]
                categoryOrderMap[catId] = counter
                categoryOrderMap[catDetails[swapIndex].Id] = swapIndex
            }
            counter++
        }
    }
}

答案 10 :(得分:0)

在mongo返回数组之后对结果进行排序的一种简单方法是创建一个id为关键字的对象,然后映射给定的_id以返回一个正确排序的数组。

async function batchUsers(Users, keys) {
  const unorderedUsers = await Users.find({_id: {$in: keys}}).toArray()
  let obj = {}
  unorderedUsers.forEach(x => obj[x._id]=x)
  const ordered = keys.map(key => obj[key])
  return ordered
}