web-dev-qa-db-ja.com

配列内のすべての要素が条件に一致するかどうかを確認します

ドキュメントのコレクションがあります:

date: Date
users: [
  { user: 1, group: 1 }
  { user: 5, group: 2 }
]

date: Date
users: [
  { user: 1, group: 1 }
  { user: 3, group: 2 }
]

このコレクションに対してクエリを実行して、ユーザーの配列内のすべてのユーザーIDが別の配列[1、5、7]にあるすべてのドキュメントを検索します。この例では、最初のドキュメントのみが一致します。

私が見つけた最善の解決策は、次のことです:

$where: function() { 
  var ids = [1, 5, 7];
  return this.users.every(function(u) { 
    return ids.indexOf(u.user) !== -1;
  });
}

残念ながら、これは $ where のドキュメントに記載されているパフォーマンスを損なうようです:

$ whereはJavaScriptを評価し、インデックスを利用できません。

このクエリを改善するにはどうすればよいですか?

26
Wex

必要なクエリは次のとおりです。

db.collection.find({"users":{"$not":{"$elemMatch":{"user":{$nin:[1,5,7]}}}}})

これは、リスト1,5,7の外にある要素を持たないすべてのドキュメントを見つけると言います。

36
Asya Kamsky

私はより良いことを知りませんが、これにアプローチするいくつかの異なる方法があり、利用可能なMongoDBのバージョンによって異なります。

これがあなたの意図であるかどうかはあまりわかりませんが、示されているクエリは最初のドキュメントの例と一致します。ロジックが実装されると、サンプルの配列に含まれる必要があるそのドキュメントの配列内の要素と一致するためです.

したがって、実際にドキュメントにこれらの要素のallを含める場合は、 $all 演算子は当然の選択です。

db.collection.find({ "users.user": { "$all": [ 1, 5, 7 ] } })

しかし、少なくとも提案に従って、 $in 演算子。これにより、評価されるJavaScriptで$where **条件の対象となるドキュメントが少なくなります。

db.collection.find({
    "users.user": { "$in": [ 1, 5, 7 ] },
    "$where": function() { 
        var ids = [1, 5, 7];
        return this.users.every(function(u) { 
            return ids.indexOf(u.user) !== -1;
        });
    }
})

そして、実際のスキャンは一致したドキュメントの配列内の要素の数で乗算されますが、追加のフィルターを使用しない場合よりも優れていますが、インデックスを取得します。

または場合によっては、 $and 演算子と $or および場合によっては $size 実際の配列条件に応じた演算子:

db.collection.find({
    "$or": [
        { "users.user": { "$all": [ 1, 5, 7 ] } },
        { "users.user": { "$all": [ 1, 5 ] } },
        { "users.user": { "$all": [ 1, 7 ] } },
        { "users": { "$size": 1 }, "users.user": 1 },
        { "users": { "$size": 1 }, "users.user": 5 },
        { "users": { "$size": 1 }, "users.user": 7 }
    ]
})

したがって、これは一致する条件のすべての可能な順列の世代ですが、パフォーマンスはインストールされている使用可能なバージョンによって異なる可能性があります。

注:実際、この場合は完全に失敗します。これはまったく異なることを行い、実際には論理的$in


別の方法は集約フレームワークを使用します。コレクション内のドキュメントの数、MongoDB 2.6以降での1つのアプローチのために、最も効率的です。

db.problem.aggregate([
    // Match documents that "could" meet the conditions
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Keep your original document and a copy of the array
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
    }},

    // Unwind the array copy
    { "$unwind": "$users" },

    // Just keeping the "user" element value
    { "$group": {
        "_id": "$_id",
        "users": { "$Push": "$users.user" }
    }},

    // Compare to see if all elements are a member of the desired match
    { "$project": {
        "match": { "$setEquals": [
            { "$setIntersection": [ "$users", [ 1, 5, 7 ] ] },
            "$users"
        ]}
    }},

    // Filter out any documents that did not match
    { "$match": { "match": true } },

    // Return the original document form
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

そのため、このアプローチでは、内容を比較するために、新しく導入された set operator を使用していますが、もちろん配列を再構築する必要があります比較するために。

指摘されているように、 $setIsSubset でこれを行う直接演算子があります。単一の演算子の上の演算子:

db.collection.aggregate([
    { "$match": { 
        "users.user": { "$in": [ 1,5,7 ] } 
    }},
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
    }},
    { "$unwind": "$users" },
    { "$group": {
        "_id": "$_id",
        "users": { "$Push": "$users.user" }
    }},
    { "$project": {
        "match": { "$setIsSubset": [ "$users", [ 1, 5, 7 ] ] }
    }},
    { "$match": { "match": true } },
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

または、MongoDB 2.6の $size 演算子を利用しながら、別のアプローチで:

db.collection.aggregate([
    // Match documents that "could" meet the conditions
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Keep your original document and a copy of the array
    // and a note of it's current size
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
        "size": { "$size": "$users" }
    }},

    // Unwind the array copy
    { "$unwind": "$users" },

    // Filter array contents that do not match
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Count the array elements that did match
    { "$group": {
        "_id": "$_id",
        "size": { "$first": "$size" },
        "count": { "$sum": 1 }
    }},

    // Compare the original size to the matched count
    { "$project": { 
        "match": { "$eq": [ "$size", "$count" ] } 
    }},

    // Filter out documents that were not the same
    { "$match": { "match": true } },

    // Return the original document form
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

2.6より前のバージョンではもう少し長くなりますが、もちろんこれはまだ可能です。

db.collection.aggregate([
    // Match documents that "could" meet the conditions
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Keep your original document and a copy of the array
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
    }},

    // Unwind the array copy
    { "$unwind": "$users" },

    // Group it back to get it's original size
    { "$group": { 
        "_id": "$_id",
        "users": { "$Push": "$users" },
        "size": { "$sum": 1 }
    }},

    // Unwind the array copy again
    { "$unwind": "$users" },

    // Filter array contents that do not match
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Count the array elements that did match
    { "$group": {
        "_id": "$_id",
        "size": { "$first": "$size" },
        "count": { "$sum": 1 }
    }},

    // Compare the original size to the matched count
    { "$project": { 
        "match": { "$eq": [ "$size", "$count" ] } 
    }},

    // Filter out documents that were not the same
    { "$match": { "match": true } },

    // Return the original document form
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

それは一般的にさまざまな方法をまとめ、それらを試してみて、あなたに最適なものを見てください。おそらく、 $in と既存のフォームの単純な組み合わせが、おそらく最良の組み合わせになるでしょう。ただし、すべての場合において、選択可能なインデックスがあることを確認してください。

db.collection.ensureIndex({ "users.user": 1 })

ここにあるすべての例がそうであるように、何らかの方法でアクセスしている限り、最高のパフォーマンスが得られます。


評決

私はこれに興味をそそられたため、最高のパフォーマンスが得られたものを確認するために、最終的にテストケースを考案しました。そのため、最初にいくつかのテストデータを生成します。

var batch = [];
for ( var n = 1; n <= 10000; n++ ) {
    var elements = Math.floor(Math.random(10)*10)+1;

    var obj = { date: new Date(), users: [] };
    for ( var x = 0; x < elements; x++ ) {
        var user = Math.floor(Math.random(10)*10)+1,
            group = Math.floor(Math.random(10)*10)+1;

        obj.users.Push({ user: user, group: group });
    }

    batch.Push( obj );

    if ( n % 500 == 0 ) {
        db.problem.insert( batch );
        batch = [];
    }

} 

1..0のランダムな値を保持する長さ1..10のランダムな配列を持つコレクション内の10000ドキュメントで、430ドキュメントの一致カウントになりました(7749から$inmatch)次の結果(avg):

  • JavaScript($in句:420ms
  • $sizeで集計:395ms
  • グループ配列数で集約:650ms
  • 2つの集合演算子を使用した集約:275ms
  • $setIsSubsetで集約:250ms

最後の2つを除くすべてのサンプルでpeak分散が約100ミリ秒速くなり、最後の2つは220ミリ秒の応答を示したことに注意してください。最大のバリエーションはJavaScriptクエリで、100ミリ秒遅い結果も示されました。

しかし、ここでのポイントはハードウェアに関連しており、ラップトップではVMは特に素晴らしいものではありませんが、アイデアを与えてくれます。

したがって、集合体、具体的には集合演算子を含むMongoDB 2.6.1バージョンは、$setIsSubsetからのわずかなゲインを追加することで、明らかにパフォーマンスで勝ちます。単一の演算子。

これは特に興味深いものです(2.4互換方式で示されているように)このプロセスの最大コストは$unwindステートメント(100ミリ秒以上) avg)、したがって、$inを選択すると、平均が約32ミリ秒で、残りのパイプラインステージは平均で100ミリ秒未満で実行されます。そのため、集計とJavaScriptのパフォーマンスの相対的な考え方が得られます。

12
Neil Lunn

厳密な平等ではなく、オブジェクトの比較を使用して、上記のAsyaのソリューションを実装しようとして、かなりの時間を費やしました。だから私はここでそれを共有すると思った。

質問をuserIdsから完全なユーザーに拡張したとします。 users配列内のすべてのアイテムが別のユーザー配列に存在するすべてのドキュメントを検索する場合:_[{user: 1, group: 3}, {user: 2, group: 5},...]_

これは機能しません:db.collection.find({"users":{"$not":{"$elemMatch":{"$nin":[{user: 1, group: 3},{user: 2, group: 5},...]}}}}})は$ ninが厳密な等価性に対してのみ機能するためです。そのため、オブジェクトの配列に対して「配列ではない」という別の表現方法を見つける必要があります。また、_$where_を使用すると、クエリが非常に遅くなります。

解決:

_db.collection.find({
 "users": {
   "$not": {
     "$elemMatch": {
       // if all of the OR-blocks are true, element is not in array
       "$and": [{
         // each OR-block == true if element != that user
         "$or": [
           "user": { "ne": 1 },
           "group": { "ne": 3 }
         ]
       }, {
         "$or": [
           "user": { "ne": 2 },
           "group": { "ne": 5 }
         ]
       }, {
         // more users...
       }]
     }
   }
 }
})
_

ロジックを仕上げるには:$ elemMatchは、配列にないユーザーを持つすべてのドキュメントに一致します。したがって、$ notは、配列内のすべてのユーザーを含むすべてのドキュメントに一致します。

0
Mark Bryk