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


Answers:


78

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

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

因此,假设您正在_id将文档中的值与要传递给$inas的数组进行匹配[ 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;
        }
    }
)

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


因此,从本质上讲,这些是将输入列表的顺序保持$in为确定的顺序的一种方式。


2
好答案。对于那些需要它的人,这里
Lawrence Jones

1
@NeilLunn我尝试了使用聚合的方法,但是得到了ID和权重。您知道如何检索帖子(对象)吗?
Juanjo Lainez Reche 2014年

1
@NeilLunn我实际上做了(这里是stackoverflow.com/questions/27525235/…),但是唯一的评论是指这里,即使我在发布问题之前已经检查了它。你能帮我吗?谢谢!
Juanjo Lainez Reche 2014年

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; }}发出(命令,{doc:this});};
NoobSter '16

1
如果你想拥有所有原始字段也可以用“$ addFields”而不是“$项目”
净土

39

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

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

按此顺序获取示例文档-

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


有没有一种方法可以将order数组存储为查询中的变量,所以如果数组很大,就不会两次对同一数组进行大规模查询?
伊桑·SK

24

如果您不想使用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版本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 '16

1
@lboyel我并不是说它那么聪明:-),但是行得通,因为它使用Mongoose的Document#equals文档与文档_id字段进行比较。更新以使_id比较明确。感谢您的询问。
JohnnyHK '16

6

JonnyHK的解决方案类似,您可以find结合使用EcmaScript 2015中的mapArray.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);
        });
    });

});

一些注意事项:

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

5

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

4

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



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++
        }
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.