51

The MongoDB docs on the $in conditional operator don't say anything about order. If I run a query of the form

db.things.find({'_id': {'$in': id_array}});

what will be the order of the returned results? And is there a way for me to tell MongoDB "I want the results sorted so that they're in the same order as the ids in id_array?"

Trevor Burnham
  • 76,828
  • 33
  • 160
  • 196

6 Answers6

11

Asked for this feature on JIRA:

Quickly got a pretty good response: use $or instead of $in

c.find( { _id:{ $in:[ 1, 2, 0 ] } } ).toArray()

vs.

c.find( { $or:[ { _id:1 }, { _id:2 }, { _id:0 } ] } ).toArray()

Read the bug report for more info.

Update:

The $or work-around hack no longer works starting with 2.6.x - it was a side effect of implementation which has changed.

Jyotman Singh
  • 10,792
  • 8
  • 39
  • 55
Jonathan Ong
  • 19,927
  • 17
  • 79
  • 118
7

I had the same problem and my solution was to create a hash map to do the mapping between my array of ids (my query) and my array of results from MongoDB.

The extra work is to browse the array of results and to insert, for each item, a new key-value pair: the key is the ID and the value is the result object.

Then, when I want to browse my results in the same order as my query was, I can use the hashmap to retrieve the correct object. No sorting, and no fancy Mongo DB option.

In Javascript it would be something like:

//The order you want
var queryIds = [ 8, 5, 3, 7 ];

//The results from MongoDB in an undefined order
var resultsFromMongoDB = [
    {_id: 7, data: "d" },
    {_id: 8, data: "a" },
    {_id: 5, data: "b" },
    {_id: 3, data: "c" },
];

//The function to create a hashmap to do the mapping
function createHashOfResults( results ){
    var hash = {};

    for( var i = 0 ; i < results.length ; i++ ){
        var item = results[i];
        var id = item._id;
        hash[ id ] = item;
    }

    return hash;
}

//Let's build the hashmap
var hash = createHashOfResults( resultsFromMongoDB );

//Now we can display the results in the order we want
for( var i = 0 ; i < queryIds.length ; i++ ){
    var queryId = queryIds[i];
    var result = hash[queryId];
    console.log( result.data );
}

This will display:

a
b
c
d
Jason
  • 920
  • 8
  • 19
6

@Jason 's answer is the correct one.

About other answers: I wouldn't recommend querying one by one because it can bring serious performance issues.

In addition to @Jason 's answer, it can be optimised using Array.reduce and Array.map methods, like this:

//The order you want
var queryIds = [8, 5, 3, 7];

//The results from MongoDB in an undefined order
var resultsFromMongoDB = [
    {_id: 7, data: "d"},
    {_id: 8, data: "a"},
    {_id: 5, data: "b"},
    {_id: 3, data: "c"}
];

var reorderedResults = naturalOrderResults(resultsFromMongoDB, queryIds);


function naturalOrderResults(resultsFromMongoDB, queryIds) {
    //Let's build the hashmap
    var hashOfResults = resultsFromMongoDB.reduce(function (prev, curr) {
        prev[curr._id] = curr;
        return prev;
    }, {});

    return queryIds.map( function(id) { return hashOfResults[id] } );
}
David Rissato Cruz
  • 3,347
  • 2
  • 17
  • 17
4

the order of the results isn't mentioned because they won't be ordered in any dependable way. the only way to get them ordered would be to do separate queries client-side for each item in the $in array

mdirolf
  • 7,521
  • 2
  • 23
  • 15
  • OK, that's what I expected. So let's say I'm making the query via PyMongo. Then I can either: 1. Make a separate MongoDB for each id in id_array and append it to the end of a list (inefficient because of the excess queries), or 2. Make the single MongoDB query given in my question, then use Python to copy the results from MongoDB into a properly ordered list (inefficient because I'm copying potentially large amounts of data on the client side)? There isn't a better way? Some kind of custom query I can run on MongoDB that will sort the results before they're returned? – Trevor Burnham Jun 29 '10 at 17:43
  • you could change your schema. there is actually a question going on the mongodb-user list right now discussing exactly that option, for exactly this question. should probably just check that out and follow up there. – mdirolf Jun 29 '10 at 18:49
  • 3
    I might be a bit late to this party, but I've just come across this problem. My solution was to build a dictionary of (in my case) `ObjectId` to position (that is, position in the list I used for the `$in` query), and then use that to rearrange the output. I am operating on a small number of items, though, so perhaps this isn't helpful for your case. – dcrosta Mar 10 '11 at 20:20
  • 1
    I've basically resorted to the same mechanism as @dcrosta: manually re-ordering the result using the original "in list" ordering as reference. I remember this being a problem with SQL databases as well, so not a Mongo peculiarity. – Cornel Masson Sep 15 '11 at 09:37
4

If you don't mind using Underscore.js and aren't too worried about scale (IE, you don't mind fetch-ing rather than working with the cursor) , here's how I maintained order:

var results = db.things.find({'_id': {'$in': id_array}}).fetch(); 
return _.sortBy(results, function(thing) { 
  return id_array.indexOf(thing._id); 
});
Alexey
  • 41
  • 2
  • i see a tradeoff here: This approach looks cleaner but it may use more CPU. This happens because array.indexOf will scan id_array for every item in your results array and after all this work it ends resorting all results. Check for yourself: https://jsfiddle.net/davidrc/gyjrtovu/ Of course this isn't a problem to front-ends (we are talking about some ms), but may put an extra load when we run this code in the backend with tons of users. – David Rissato Cruz Jan 14 '16 at 16:37
  • Agreed. I had to do this for <50 entries so being obvious with intent and brevity was my first priority. – Alexey Jan 16 '16 at 21:08
2

The $or workaround no longer works in 2.6.x, see https://jira.mongodb.org/browse/SERVER-14083. Here's a Ruby workaround implementation: https://gist.github.com/dblock/d5ed835f0147467a6a27

dB.
  • 4,700
  • 2
  • 46
  • 51