MongoDB 聚合 - 使用 lte 匹配并回退到 gte
MongoDB aggregate - match using lte and fallback to gte
我有一个 MongoDB 集合,其结构(简化)如下:
[
{
"name" : "name1",
"instances" : [
{
"value" : 1,
"score" : 2,
"date" : ISODate("2015-03-04T00:00:00.000Z")
},
{
"value" : 2,
"score" : 5,
"date" : ISODate("2015-04-01T00:00:00.000Z")
},
{
"value" : 2.5,
"score" : 9,
"date" : ISODate("2015-03-05T00:00:00.000Z")
}
]
},
{
"name" : "name2",
"instances" : [
{
"value" : 6,
"score" : 3,
"date" : ISODate("2015-03-05T00:00:00.000Z")
},
{
"value" : 1,
"score" : 6,
"date" : ISODate("2015-03-04T00:00:00.000Z")
},
{
"value" : 3.7,
"score" : 5.2,
"date" : ISODate("2015-02-04T00:00:00.000Z")
}
]
},
{
"name" : "name3",
"instances" : [
{
"value" : 6,
"score" : 3,
"date" : ISODate("2015-03-05T00:00:00.000Z")
},
{
"value" : 1,
"score" : 6,
"date" : ISODate("2015-03-04T00:00:00.000Z")
},
{
"value" : 3.7,
"score" : 5.2,
"date" : ISODate("2015-02-04T00:00:00.000Z")
}
]
}
]
目前我有一个 aggregate
查询,它在给定日期之前从每个文档中提取单个实例:
db.myCollection.aggregate([
{$unwind: "$instances"},
{$sort: {'instances.date': -1}},
{$match: {'instances.date': {$lte: <givenDate>}}},
{$project: {name: 1, _id: 0, date: "$instances.date", value: "$instances.value", score: "$instances.score"}},
{$group: {_id: "$name", name: {$first: "$name"}, date: {$first: "$date"},
value: {$first: "$value"}, score: {$first: "$score"}}}
])
此查询工作得很好,对于给定的日期,每个文档中的 return 都是最新的(即恰好或早于给定日期的)实例。
当给定日期早于最早实例时,我的问题就开始了。例如,如果我给定的日期是 2015 年 3 月 2 日,我将不会从 name1
获得任何实例。在这种情况下,我想检索文档中可用的最早实例。
显然,我可以将此任务拆分为两个不同的查询并合并结果,但如果可能的话,我希望在单个数据库查询中实现此目标。
有什么想法吗?
流水线
试试这个管道,然后让我们一步一步来:
[
{$unwind: "$instances"},
{$project: {
_id: 0,
name: 1,
date: '$instances.date',
matches: {
$cond: [
{$lte: ['$instances.date', new Date(<YOUR DATE>)]},
1,
0
]
},
score: '$instances.score',
value: '$instances.value'
}
},
{$group: {
_id: '$name',
instances: {
$push: {
date: '$date',
score: '$score',
value: '$value',
matches: '$matches'
}
},
hasMatches: {$sum: '$matches'}
}
},
{$unwind: "$instances"},
{$project: {
_id: 0,
name: '$_id',
date: '$instances.date',
hasMatches: '$hasMatches',
matches: '$instances.matches',
score: '$instances.score',
value: '$instances.value'
}
},
{$sort: {'name': 1, 'matches': -1, 'date': -1}},
{$group: {
_id: {name: '$name', hasMatches: '$hasMatches'},
last_date: {$last: '$date'},
last_score: {$last: '$score'},
last_value: {$last: '$value'},
first_date: {$first: '$date'},
first_score: {$first: '$score'},
first_value: {$first: '$value'}}
},
{$project: {
name: '$_id.name',
date: {$cond: ['$_id.hasMatches', '$first_date', '$last_date']},
score: {$cond: ['$_id.hasMatches', '$first_score', '$last_score']},
value: {$cond: ['$_id.hasMatches', '$first_value', '$last_value']},
_id: 0}
}
]
解释
第一个 $unwind
和 $project
阶段简单明了,我只添加了一个 matches
字段来指示未损坏的文档是否符合您的条件。
然后我们$group
备份文档,同时$sum
将matches
字段添加到新的hasMatches
。生成的文档现在包含 hasMatches
字段,该字段指示 instances
数组是否包含至少一个符合您的条件的元素。
然后,我们再次 $unwind
和 $project
,然后再次 $group
,保留 hasMatches
字段并存储 $first
和 $last
date
、value
和 score
的值以供进一步处理。
现情况如下:
如果初始数组中至少有一个元素符合条件,则在排序结果中它已作为 第一个 文档出现在它的组。
如果初始数组中没有个元素符合条件,则在排序结果中,日期最早的元素出现在所属组中的最后一个 个文档。
因此,由于我们有指示上述条件的 hasMatches
字段,以及 first_X
和 last_X
值,我们可以轻松地选择其中之一,具体取决于hasMatches
值。因此,最后的 $project
阶段正是这样做的。
结果
以下是您在评论中提到的日期的结果:
'2015-03-04':
{ "name" : "name3", "date" : ISODate("2015-03-04T00:00:00Z"), "score" : 6, "value" : 1 }
{ "name" : "name2", "date" : ISODate("2015-03-04T00:00:00Z"), "score" : 6, "value" : 1 }
{ "name" : "name1", "date" : ISODate("2015-03-04T00:00:00Z"), "score" : 2, "value" : 1 }
'2015-03-02':
{ "name" : "name3", "date" : ISODate("2015-02-04T00:00:00Z"), "score" : 5.2, "value" : 3.7 }
{ "name" : "name2", "date" : ISODate("2015-02-04T00:00:00Z"), "score" : 5.2, "value" : 3.7 }
{ "name" : "name1", "date" : ISODate("2015-03-04T00:00:00Z"), "score" : 2, "value" : 1 }
我有一个 MongoDB 集合,其结构(简化)如下:
[
{
"name" : "name1",
"instances" : [
{
"value" : 1,
"score" : 2,
"date" : ISODate("2015-03-04T00:00:00.000Z")
},
{
"value" : 2,
"score" : 5,
"date" : ISODate("2015-04-01T00:00:00.000Z")
},
{
"value" : 2.5,
"score" : 9,
"date" : ISODate("2015-03-05T00:00:00.000Z")
}
]
},
{
"name" : "name2",
"instances" : [
{
"value" : 6,
"score" : 3,
"date" : ISODate("2015-03-05T00:00:00.000Z")
},
{
"value" : 1,
"score" : 6,
"date" : ISODate("2015-03-04T00:00:00.000Z")
},
{
"value" : 3.7,
"score" : 5.2,
"date" : ISODate("2015-02-04T00:00:00.000Z")
}
]
},
{
"name" : "name3",
"instances" : [
{
"value" : 6,
"score" : 3,
"date" : ISODate("2015-03-05T00:00:00.000Z")
},
{
"value" : 1,
"score" : 6,
"date" : ISODate("2015-03-04T00:00:00.000Z")
},
{
"value" : 3.7,
"score" : 5.2,
"date" : ISODate("2015-02-04T00:00:00.000Z")
}
]
}
]
目前我有一个 aggregate
查询,它在给定日期之前从每个文档中提取单个实例:
db.myCollection.aggregate([
{$unwind: "$instances"},
{$sort: {'instances.date': -1}},
{$match: {'instances.date': {$lte: <givenDate>}}},
{$project: {name: 1, _id: 0, date: "$instances.date", value: "$instances.value", score: "$instances.score"}},
{$group: {_id: "$name", name: {$first: "$name"}, date: {$first: "$date"},
value: {$first: "$value"}, score: {$first: "$score"}}}
])
此查询工作得很好,对于给定的日期,每个文档中的 return 都是最新的(即恰好或早于给定日期的)实例。
当给定日期早于最早实例时,我的问题就开始了。例如,如果我给定的日期是 2015 年 3 月 2 日,我将不会从 name1
获得任何实例。在这种情况下,我想检索文档中可用的最早实例。
显然,我可以将此任务拆分为两个不同的查询并合并结果,但如果可能的话,我希望在单个数据库查询中实现此目标。
有什么想法吗?
流水线
试试这个管道,然后让我们一步一步来:
[
{$unwind: "$instances"},
{$project: {
_id: 0,
name: 1,
date: '$instances.date',
matches: {
$cond: [
{$lte: ['$instances.date', new Date(<YOUR DATE>)]},
1,
0
]
},
score: '$instances.score',
value: '$instances.value'
}
},
{$group: {
_id: '$name',
instances: {
$push: {
date: '$date',
score: '$score',
value: '$value',
matches: '$matches'
}
},
hasMatches: {$sum: '$matches'}
}
},
{$unwind: "$instances"},
{$project: {
_id: 0,
name: '$_id',
date: '$instances.date',
hasMatches: '$hasMatches',
matches: '$instances.matches',
score: '$instances.score',
value: '$instances.value'
}
},
{$sort: {'name': 1, 'matches': -1, 'date': -1}},
{$group: {
_id: {name: '$name', hasMatches: '$hasMatches'},
last_date: {$last: '$date'},
last_score: {$last: '$score'},
last_value: {$last: '$value'},
first_date: {$first: '$date'},
first_score: {$first: '$score'},
first_value: {$first: '$value'}}
},
{$project: {
name: '$_id.name',
date: {$cond: ['$_id.hasMatches', '$first_date', '$last_date']},
score: {$cond: ['$_id.hasMatches', '$first_score', '$last_score']},
value: {$cond: ['$_id.hasMatches', '$first_value', '$last_value']},
_id: 0}
}
]
解释
第一个 $unwind
和 $project
阶段简单明了,我只添加了一个 matches
字段来指示未损坏的文档是否符合您的条件。
然后我们$group
备份文档,同时$sum
将matches
字段添加到新的hasMatches
。生成的文档现在包含 hasMatches
字段,该字段指示 instances
数组是否包含至少一个符合您的条件的元素。
然后,我们再次 $unwind
和 $project
,然后再次 $group
,保留 hasMatches
字段并存储 $first
和 $last
date
、value
和 score
的值以供进一步处理。
现情况如下:
如果初始数组中至少有一个元素符合条件,则在排序结果中它已作为 第一个 文档出现在它的组。
如果初始数组中没有个元素符合条件,则在排序结果中,日期最早的元素出现在所属组中的最后一个 个文档。
因此,由于我们有指示上述条件的 hasMatches
字段,以及 first_X
和 last_X
值,我们可以轻松地选择其中之一,具体取决于hasMatches
值。因此,最后的 $project
阶段正是这样做的。
结果
以下是您在评论中提到的日期的结果:
'2015-03-04':
{ "name" : "name3", "date" : ISODate("2015-03-04T00:00:00Z"), "score" : 6, "value" : 1 }
{ "name" : "name2", "date" : ISODate("2015-03-04T00:00:00Z"), "score" : 6, "value" : 1 }
{ "name" : "name1", "date" : ISODate("2015-03-04T00:00:00Z"), "score" : 2, "value" : 1 }
'2015-03-02':
{ "name" : "name3", "date" : ISODate("2015-02-04T00:00:00Z"), "score" : 5.2, "value" : 3.7 }
{ "name" : "name2", "date" : ISODate("2015-02-04T00:00:00Z"), "score" : 5.2, "value" : 3.7 }
{ "name" : "name1", "date" : ISODate("2015-03-04T00:00:00Z"), "score" : 2, "value" : 1 }