我有一个Mongodb模式,大致如下:
[
{
"name" : "name1",
"instances" : [
{
"value" : 1,
"date" : ISODate("2015-03-04T00:00:00.000Z")
},
{
"value" : 2,
"date" : ISODate("2015-04-01T00:00:00.000Z")
},
{
"value" : 2.5,
"date" : ISODate("2015-03-05T00:00:00.000Z")
},
...
]
},
{
"name" : "name2",
"instances" : [
...
]
}
]
其中每个元素的实例数量可能相当大。
我有时只想得到一个数据样本,也就是说,每3个实例,或者每10个实例。。。你明白了。
我可以通过获取所有实例并在服务器代码中过滤它们来实现这个目标,但我想知道是否有一种方法可以通过使用聚合查询来实现。
有什么想法吗?
更新
假设数据结构是平坦的,正如@SylvainLeoux在下面建议的那样,即:
[
{"name": "name1", "value": 1, "date": ISODate("2015-03-04T00:00:00.000Z")},
{"name": "name2", "value": 5, "date": ISODate("2015-04-04T00:00:00.000Z")},
{"name": "name1", "value": 2, "date": ISODate("2015-04-01T00:00:00.000Z")},
{"name": "name1", "value": 2.5, "date": ISODate("2015-03-05T00:00:00.000Z")},
...
]
获得(特定name
的)每N个项目的任务会更容易吗?
您的问题似乎明确地问了"获取每n个实例",这似乎是一个非常明确的问题。
像.find()
这样的查询操作实际上只能按原样返回文档,但投影中的一般字段"选择"和允许奇异匹配数组元素的运算符(如位置$
匹配运算符或$elemMatch
)除外。
当然,有$slice
,但这只允许在阵列上进行"范围选择",因此同样不适用。
服务器上"唯一"可以修改结果的是.aggregate()
和.mapReduce()
。前者在任何方面都不能"很好地"对数组进行"切片",至少不能对"n"个元素进行切片。然而,由于mapReduce的"function()"参数是基于JavaScript的逻辑,因此您有更多的空间可以使用。
对于分析过程和"仅"用于分析目的,则只需使用.filter()
:通过mapReduce过滤数组内容
db.collection.mapReduce(
function() {
var id = this._id;
delete this._id;
// filter the content of "instances" to every 3rd item only
this.instances = this.instances.filter(function(el,idx) {
return ((idx+1) % 3) == 0;
});
emit(id,this);
},
function() {},
{ "out": { "inline": 1 } } // or output to collection as required
)
在这一点上,它实际上只是一个"JavaScript运行程序",但如果这只是为了分析/测试,那么这个概念通常没有什么问题。当然,输出并不是文档的"确切"结构,但它就像mapReduce所能得到的传真一样。
我在这里看到的另一个建议要求创建一个新的集合,其中所有项都"非规范化",并从数组中插入"索引"作为unqique _id
键的一部分。这可能会产生一些你可以直接查询的东西,但对于"每n个项目",你仍然需要做:
db.resultCollection.find({
"_id.index": { "$in": [2,5,8,11,14] } // and so on ....
})
因此,求出并提供"每n个项目"的指标值,从而得到"每n项"。因此,这似乎并不能真正解决所提出的问题。
如果输出形式似乎更适合您的"测试"目的,那么对这些结果的更好的后续查询将使用聚合管道,使用$redact
db.newCollection([
{ "$redact": {
"$cond": {
"if": {
"$eq": [
{ "$mod": [ { "$add": [ "$_id.index", 1] }, 3 ] },
0 ]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
这至少使用了一个"逻辑条件",与之前.filter()
应用的条件非常相似,只选择"第n个索引"项,而不列出所有可能的索引值作为查询参数。
此处不需要$unwind
。您可以将$push
与$arrayElemAt
一起使用,以在$group
聚合内的请求索引处投影数组值。
类似的东西
db.colname.aggregate(
[
{"$group":{
"_id":null,
"valuesatNthindex":{"$push":{"$arrayElemAt":["$instances",N]}
}}
},
{"$project":{"valuesatNthindex":1}}
])
您可能喜欢使用$lookup
聚合的这种方法。而且可能是最方便、最快的方式,而不需要任何聚合技巧。
使用以下模式创建集合Names
[
{ "_id": 1, "name": "name1" },
{ "_id": 2, "name": "name2" }
]
然后Instances
父id为"nameId"
的集合
[
{ "nameId": 1, "value" : 1, "date" : ISODate("2015-03-04T00:00:00.000Z") },
{ "nameId": 1, "value" : 2, "date" : ISODate("2015-04-01T00:00:00.000Z") },
{ "nameId": 1, "value" : 3, "date" : ISODate("2015-03-05T00:00:00.000Z") },
{ "nameId": 2, "value" : 7, "date" : ISODate("2015-03-04T00:00:00.000Z") },
{ "nameId": 2, "value" : 8, "date" : ISODate("2015-04-01T00:00:00.000Z") },
{ "nameId": 2, "value" : 4, "date" : ISODate("2015-03-05T00:00:00.000Z") }
]
现在,使用$lookup
聚合3.6语法,您可以在$lookup
pipeline
中使用$sample
来随机获取每个Nth元素。
db.Names.aggregate([
{ "$lookup": {
"from": Instances.collection.name,
"let": { "nameId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$nameId", "$$nameId"] }}},
{ "$sample": { "size": N }}
],
"as": "instances"
}}
])
您可以在此处测试
不幸的是,使用聚合框架是不可能的,因为这需要$unwind
的选项来发出数组索引/位置,而当前聚合无法处理该索引/位置。这里有一张未结的JIRA票证SERVER-4588。
然而,一个解决方法是使用MapReduce,但这会带来巨大的性能成本,因为获取数组索引的实际计算是使用嵌入式JavaScript引擎执行的(速度很慢),并且仍然有一个全局JavaScript锁,它只允许一个JavaScript线程在一次运行。
使用mapReduce,您可以尝试以下操作:
映射功能:
var map = function(){
for(var i=0; i < this.instances.length; i++){
emit(
{ "_id": this._id, "index": i },
{ "index": i, "value": this.instances[i] }
);
}
};
减少功能:
var reduce = function(){}
然后,您可以在集合上运行以下mapReduce
函数:
db.collection.mapReduce( map, reduce, { out : "resultCollection" } );
然后,您可以使用map()
游标方法查询结果集合,以获取实例数组的每N项的列表/数组:
var thirdInstances = db.resultCollection.find({"_id.index": N})
.map(function(doc){return doc.value.value})
您可以使用以下聚合:
db.col.aggregate([
{
$project: {
instances: {
$map: {
input: { $range: [ 0, { $size: "$instances" }, N ] },
as: "index",
in: { $arrayElemAt: [ "$instances", "$$index" ] }
}
}
}
}
])
$range生成一个索引列表。第三个参数表示非零步长。对于N = 2
,它将是[0,2,4,6...]
,对于N = 3
,它将返回[0,3,6,9...]
,依此类推。然后您可以使用$map从instances
数组中获取相应的项。
或者只有一个查找块:
db.Collection.find({}).then(function(data) {
var ret = [];
for (var i = 0, len = data.length; i < len; i++) {
if (i % 3 === 0 ) {
ret.push(data[i]);
}
}
return ret;
});
返回一个promise,您可以调用它的then()来获取第N个模块化数据。