Ответ 1
Запрос, который вы хотите, следующий:
db.collection.find({"users":{"$not":{"$elemMatch":{"user":{$nin:[1,5,7]}}}}})
Это говорит мне найти все документы, которые не имеют элементов, которые находятся за пределами списка 1,5,7.
У меня есть набор документов:
date: Date
users: [
{ user: 1, group: 1 }
{ user: 5, group: 2 }
]
date: Date
users: [
{ user: 1, group: 1 }
{ user: 3, group: 2 }
]
Я хотел бы запросить эту коллекцию, чтобы найти все документы, в которых каждый идентификатор пользователя в моем массиве пользователей находится в другом массиве, [1, 5, 7]. В этом примере соответствует только первый документ.
Лучшее решение, которое я смог найти, это сделать:
$where: function() {
var ids = [1, 5, 7];
return this.users.every(function(u) {
return ids.indexOf(u.user) !== -1;
});
}
К сожалению, это, по-видимому, ухудшает производительность, говорится в $где docs:
$, где оценивает JavaScript и не может использовать индексы.
Как я могу улучшить этот запрос?
Запрос, который вы хотите, следующий:
db.collection.find({"users":{"$not":{"$elemMatch":{"user":{$nin:[1,5,7]}}}}})
Это говорит мне найти все документы, которые не имеют элементов, которые находятся за пределами списка 1,5,7.
Я не знаю о лучшем, но есть несколько разных способов приблизиться к этому, и в зависимости от версии MongoDB у вас есть.
Не уверен, что это ваше намерение или нет, но запрос, как показано, будет соответствовать первому примеру документа, поскольку по мере реализации вашей логики вы соответствуете элементам внутри этого массива документов, которые должны содержаться в массиве образцов.
Итак, если вы действительно хотели, чтобы документ содержал все этих элементов, $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 и выше:
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"
}}
])
Таким образом, этот подход использует некоторые недавно введенные установить операторы, чтобы сравнить содержимое, хотя, конечно, вам нужно для реструктуризации массива для сравнения.
Как указано, существует прямой оператор для этого в $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"
}}
])
Или с другим подходом, все еще использующим оператор $size
от MongoDB 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
// and a note of it 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 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 = [];
}
}
С 10000 документами в коллекции со случайными массивами от 1..10 в длину, содержащими случайные значения 1..0, я пришел к совпадению 430 документов (уменьшено с 7749 от $in
) со следующими результатами (avg):
$in
: 420 мс$size
: 395 мс$setIsSubset
: 250 мсОтмечая, что по образцам, выполненным все, кроме двух последних, была максимальная дисперсия приблизительно на 100 мс быстрее, а последние два показали 220 мс ответ. Самые большие вариации были в запросе JavaScript, который также показывал результаты на 100 мс медленнее.
Но точка здесь относительно аппаратного обеспечения, которое на моем ноутбуке под VM не особенно велико, но дает представление.
Таким образом, совокупность, и в частности версия MongoDB 2.6.1 с установленными операторами, явно выигрывает от производительности с дополнительным небольшим усилением от $setIsSubset
как одного оператора.
Это особенно интересно (как показано в 2.4-совместимом методе) наибольшая стоимость этого процесса будет заключаться в выражении $unwind
(более 100 мс), поэтому с t22 > , имеющий среднее значение около 32 мс, остальные этапы конвейера выполняются в среднем менее чем за 100 мс. Таким образом, это дает относительное представление об агрегации и производительности JavaScript.
Я потратил большую часть своего времени на то, чтобы попытаться реализовать решение 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 соответствует всем документам, у которых есть пользователь, не входящий в массив. Таким образом, $не будет соответствовать всем документам, в которых есть все пользователи в массиве.