MongoDB的$in子句是否保证顺序?

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

9
这是一个有关该功能的MongoDB工单链接。 - Mitar
11个回答

94

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

如果您需要保留此顺序,则基本上有两个选项。

假设您要匹配文档中_id的值,并将其数组作为$in参数传递,例如[ 4, 2, 8 ]

使用聚合方法


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

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


因此,在您已经确定了列表顺序的情况下,这些基本上是维护输入列表顺序到$in条件的方式。


2
非常好的答案。如果需要,这里提供一个 CoffeeScript 版本 [here] (https://gist.github.com/e1ff8a85e32af6ff9a0e) - Lawrence Jones
1
@NeilLunn 我尝试使用聚合的方法,但是我获取到了id和权重。你知道如何检索帖子(对象)吗? - Juanjo Lainez Reche
1
@NeilLunn 我确实做了(在这里https://dev59.com/t4bca4cB1Zd3GeqPSCar)。但是唯一的评论是指向这里,尽管在发布问题之前我已经检查过了。你能帮我吗?谢谢! - Juanjo Lainez Reche
1
我知道这是老问题了,但我浪费了很多时间调试为什么inputs.indexOf()与this._id不匹配。如果你只是返回对象Id的值,你可能需要选择这种语法:obj.map = function() { for(var i = 0; i < inputs.length; i++){ if(this._id.equals(inputs[i])) { var order = i; } } emit(order, {doc: this}); }; - NoobSter
2
如果您想要保留所有原始字段,可以使用"$addFields"代替"$project"。 - Jodo
显示剩余4条评论

62

使用聚合查询的另一种方式,仅适用于MongoDB版本>= 3.4 -

功劳归于这篇不错的博客文章

要按此顺序获取的示例文档 -

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字段表示我们提供的数组的原始顺序。然后,我们只需根据此字段对文档进行排序即可。


1
有没有一种方法可以将订单数组作为查询中的变量存储,以便如果数组很大,我们不必两次使用相同的大型查询来处理它? - Ethan SK
有没有想法如何将此转换为C# LINQ? - Nicholas

34

如果您不想使用 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,那么在这种情况下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,使用underscore/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); });
});

等值函数如何知道将一个ID属性与ID“return a.equals(id);”进行比较,因为a保存了该模型返回的所有属性? - lboyel
1
@lboyel 我并没有想要它那么聪明 :-),但是这个方法能够运行是因为它使用了 Mongoose 的 Document#equals 方法来比较文档的 _id 字段。我已经更新代码,使得 _id 比较更加明确。感谢您的提问。 - JohnnyHK

8

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

1
这正好符合我的需求,而且比顶部评论简单得多。 - dyarbrough
@dyarbrough 这个解决方案仅适用于获取所有文档(没有限制或跳过)的查询。顶部评论更复杂,但适用于每种情况。 - marian2js

6

JonnyHK的解决方案类似,如果您的客户端使用的是JavaScript,您可以使用map和EcmaScript 2015中的Array.prototype.find函数组合来重新排序从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
  • idArray是一个ObjectId数组
  • 我没有测试这种方法与排序的性能,但如果您需要操作每个返回的项目(这很常见),您可以在map回调中完成它以简化您的代码。

运行时间为O(n*n),因为内部的find对于数组中的每个元素(来自外部的map)都要遍历数组。这是非常低效的,因为可以使用查找表的O(n)解决方案。 - curran

5

我知道这个问题与Mongoose JS框架有关,但是重复的问题是通用的,所以我希望在这里发布一个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

3
始终如此?从未如此。顺序总是相同的:未定义(可能是文件存储的物理顺序)。除非您对其进行排序。

$natural顺序通常是逻辑顺序而非物理顺序。 - Sammaye

3

对于任何新手,这是一个简短而优雅的解决方案,可保留在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操作符中必须投影一些其他字段,我的意思是,你不能只投影订单。 - David Corral

1

0

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

因此,请改用$or: [ _ids.map(_id => ({_id}))]


2
$or 的解决方法自 v2.6 以来就已经失效了。参考链接 - JohnnyHK

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接