【问题标题】:Does MongoDB's $in clause guarantee orderMongoDB $in 子句是否保证顺序
【发布时间】:2014-05-12 22:17:05
【问题描述】:

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

【问题讨论】:

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


【解决方案1】:

总是?绝不。顺序始终相同:未定义(可能是文档存储的物理顺序)。除非你对其进行排序。

【讨论】:

  • $natural 通常是逻辑而不是物理的顺序
【解决方案2】:

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

如果你需要保留这个顺序,那么你基本上有两种选择。

假设您在文档中匹配_id 的值,该数组将作为[ 4, 2, 8 ] 传递给$in

使用聚合的方法


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 条件的方法,您已经以确定的顺序拥有该列表。

【讨论】:

  • 很好的答案。对于那些需要它的人,coffeescript 版本here
  • @NeilLunn 我尝试了使用聚合的方法,但我得到了 id 和重量。你知道如何检索帖子(对象)吗?
  • @NeilLunn 我确实做到了(它在这里 stackoverflow.com/questions/27525235/… )但唯一的评论是指这里,即使我在发布我的问题之前检查了这个。你能帮我吗?谢谢!
  • 知道这是旧的,但我浪费了很多时间调试为什么 inputs.indexOf() 与 this._id 不匹配。如果您只是返回对象 ID 的值,您可能必须选择以下语法:obj.map = function() { for(var i = 0; i
  • 如果您也想拥有所有原始字段,可以使用“$addFields”而不是“$project”
【解决方案3】:

如果您不想使用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 值是非原始类型,如ObjectIds,则需要另一种方法,因为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 的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); });
});

【讨论】:

  • equal 函数如何知道将 id 属性与 id 'return a.equals(id);' 进行比较,导致 a 保存为该模型返回的所有属性?
  • @lboyel 我并不是说它那么聪明 :-),但这很有效,因为它使用 Mongoose 的 Document#equals 与文档的 _id 字段进行比较。更新以使 _id 比较明确。感谢您的提问。
【解决方案4】:

您可以使用 $or 子句来保证顺序。

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

【讨论】:

【解决方案5】:

JonnyHK 的解决方案类似,您可以使用map 和EcmaScript 2015 中的Array.prototype.find 函数的组合,在您的客户端(如果您的客户端使用JavaScript)中重新排序从find 返回的文档:

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);
        });
    });

});

几点说明:

  • 以上代码使用的是 Mongo Node 驱动,不是 Mongoose
  • idArrayObjectId 的数组
  • 我还没有测试过这种方法与排序的性能,但是如果您需要操作每个返回的项目(这很常见),您可以在 map 回调中进行操作以简化您的代码。

【讨论】:

  • 运行时间为 O(n*n),因为内部 find 遍历数组以获取数组的每个元素(从外部 map 开始)。这是非常低效的,因为有一个使用查找表的 O(n) 解决方案。
【解决方案6】:

这是从 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++
        }
    }
}

【讨论】:

    【解决方案7】:

    我知道这是一个旧线程,但如果您只是返回数组中 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?

    【讨论】:

      【解决方案8】:

      我知道这个问题与 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
      

      【讨论】:

        【解决方案9】:

        另一种使用聚合查询的方式仅适用于 MongoDB 版本 >= 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”表达式返回给定数组中特定元素的位置。

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

        【讨论】:

        • 有没有办法将订单数组作为变量存储在查询中,这样如果数组很大,我们就不会对同一个数组进行两次大规模查询?
        【解决方案10】:

        在 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
        }
        

        【讨论】:

        • 这正是我所需要的,而且比顶级评论简单得多。
        • @dyarbrough 此解决方案仅适用于获取所有文档的查询(无限制或跳过)。顶部评论更复杂,但适用于所有场景。
        【解决方案11】:

        对于任何新手来说,这里有一个简短而优雅的解决方案,可以在 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 } },
            ])
        

        【讨论】:

        • 太棒了!谢谢。另请注意,由于某种原因,$project 运算符中必须有一些其他字段要投影,我的意思是,您不能只投影订单。
        猜你喜欢
        • 2012-12-17
        • 1970-01-01
        • 1970-01-01
        • 2011-08-31
        • 1970-01-01
        • 2021-10-09
        • 2014-09-10
        相关资源
        最近更新 更多