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

use*_*880 73 mapreduce mongoose mongodb mongodb-query aggregation-framework

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

Nei*_*unn 71

如上所述,$ in子句数组中参数的顺序不反映文档的检索顺序.那当然是自然顺序或所选择的索引顺序如图所示.

如果您需要保留此订单,那么您基本上有两个选项.

因此,假设_id您使用将要传递给$inas 的数组匹配文档中的值[ 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 } }

])
Run Code Online (Sandbox Code Playgroud)

这就是扩展形式.这里基本上发生的是,正如传递给$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 );
Run Code Online (Sandbox Code Playgroud)

使用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;
        }
    }
)
Run Code Online (Sandbox Code Playgroud)

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


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

  • 好答案。对于那些需要它的人,可以在[此处](https://gist.github.com/e1ff8a85e32af6ff9a0e)编写咖啡脚本版本 (2认同)
  • 如果您也想拥有所有原始字段,可以使用“$addFields”而不是“$project” (2认同)

Jyo*_*ngh 25

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

归功于这篇精彩的博文.

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

var order = [ "David", "Charlie", "Tess" ];
Run Code Online (Sandbox Code Playgroud)

查询 -

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

var result = db.users.aggregate(query);
Run Code Online (Sandbox Code Playgroud)

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

"$ addFields"阶段是3.4中的新增功能,它允许您在不知道所有其他现有字段的情况下"$ project"新字段到现有文档.新的"$ indexOfArray"表达式返回给定数组中特定元素的位置.

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


Joh*_*yHK 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);
    });
});
Run Code Online (Sandbox Code Playgroud)

如果$in值是非原始类型,如ObjectIds,则indexOf在这种情况下需要另一种方法作为参考比较.

如果您使用的是Node.js 4.x +,则可以通过将函数更改为以下来使用Array#findIndex和ObjectID#equals处理此问题sort:

docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) - 
                    ids.findIndex(id => b._id.equals(id)));
Run Code Online (Sandbox Code Playgroud)

或者使用任何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); });
});
Run Code Online (Sandbox Code Playgroud)


Arn*_*sen 7

在 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
}
Run Code Online (Sandbox Code Playgroud)


teb*_*200 5

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

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

});
Run Code Online (Sandbox Code Playgroud)

一些注意事项:

  • 上面的代码使用的是Mongo Node驱动程序而不是 Mongoose
  • 的idArray是阵列ObjectId
  • 我尚未测试此方法与排序的性能,但是如果您需要操纵每个返回的项(这很常见),则可以在map回调中进行操作以简化代码。


Den*_*zov 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
Run Code Online (Sandbox Code Playgroud)