MongoDBの$ in句は順序を保証しますか


回答:


80

前述のように、$ in句の配列内の引数の順序は、ドキュメントの取得方法の順序を反映していません。もちろん、それは自然な順序、または示されているように選択されたインデックスの順序になります。

この順序を維持する必要がある場合は、基本的に2つのオプションがあります。

したがって_id、ドキュメント内のの値を、$inとして渡される配列と照合していたとしましょう。[ 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 } }

])

これが拡張フォームになります。ここで基本的に行われるのは、値の配列が渡されるのと同じように、$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
素晴らしい答え。それを必要とする人のために、ここに
ローレンスジョーンズ

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; }} Emmit(order、{doc:this}); };
NoobSter 2016

1
元のフィールドもすべて取得したい場合は、「$ project」の代わりに「$ addFields」を使用できます
Jodo

40

のみ適用集計クエリ使用して別の方法のMongoDB verion> = 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フィールドは、指定した配列の元の順序を表します。次に、このフィールドに基づいてドキュメントを並べ替えるだけです。


配列が大きい場合に同じ配列のこの大規模なクエリが2回発生しないように、順序配列を変数としてクエリに格納する方法はありますか?
イーサンSK

27

を使用したくない場合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バージョンで、アンダースコア/ 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); 'と比較することをどのように知っていますか?そのモデルに対して返されるすべてのプロパティを保持しますか?
lboyel 2016年

1
@lboyel私はそれがそれほど賢いという意味ではありませんでした:-)、しかしそれはDocument#equalsドキュメントの_idフィールドと比較するためにマングースを使用していたのでうまくいきました。_id比較を明示的にするために更新されました。質問してくれてありがとう。
JohnnyHK 2016年

6

JonnyHKのソリューションと同様に、EcmaScript 2015の関数とのfind組み合わせを使用して、クライアントから返されたドキュメントを並べ替えることができます(クライアントがJavaScriptの場合)。mapArray.prototype.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);
        });
    });

});

いくつかの注意事項:

  • 上記のコードは、MongooseではなくMongoNodeドライバーを使用しています
  • idArray配列ですObjectId
  • このメソッドとソートのパフォーマンスはテストしていませんが、返された各アイテムを操作する必要がある場合(これはかなり一般的です)、mapコールバックで操作してコードを簡略化できます。

実行時間はO(n * n)です。これは、内側findが配列の各要素の配列を(外側からmap)トラバースするためです。ルックアップテーブルを使用するO(n)ソリューションがあるため、これはひどく非効率的です。
カラン

5

この質問はMongooseJSフレームワークに関連していることは知っていますが、複製されたものは一般的なものなので、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

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

3

常に?決して。順序は常に同じです:未定義(おそらくドキュメントが保存される物理的な順序)。あなたがそれを分類しない限り。


$natural物理的ではなく論理的である通常の注文
Sammaye 2014

1

これが古いスレッドであることはわかっていますが、配列内のIdの値を返すだけの場合は、この構文を選択する必要があるかもしれません。indexOf値をmongoObjectId形式と一致させることができなかったようです。

  obj.map = function() {
    for(var i = 0; i < inputs.length; i++){
      if(this._id.equals(inputs[i])) {
        var order = i;
      }
    }
    emit(order, {doc: this});
  };

'ObjectId()'ラッパーを含めずにmongo ObjectId .toStringを変換する方法-値だけですか?



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++
        }
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.