假设我们收到了一组用户,每个用户都有BSON类型日期格式的生日。
我们如何运行查询以找出在接下来的30天内过生日的所有用户?
答案 0 :(得分:9)
聚合框架肯定是正确的方法 - 在服务器上需要JS的任何东西都是性能问题,而聚合都在本机代码中在服务器中运行。
虽然可以将生日转换为即将到来的生日日期,然后进行范围查询,但我更喜欢自己做一个稍微不同的方式。
唯一的“先决条件是计算今天的今天”。 There are ways to do this in various languages,所以这可以在调用聚合之前在应用程序层中完成,并将此数字传递给它。我打算打电话给我todayDayOfYear
,但我意识到你可以让聚合框架根据今天来计算它,所以唯一的变量将是今天的日期。
var today=new Date();
我假设包含姓名和生日的文件,适当调整变化
var p1 = { "$project" : {
"_id" : 0,
"name" : 1,
"birthday" : 1,
"todayDayOfYear" : { "$dayOfYear" : today },
"dayOfYear" : { "$dayOfYear" : "$birthday"}
} };
现在,预测从今天到下一个生日的天数:
var p2 = { "$project" : {
"name" : 1,
"birthday" : 1,
"daysTillBirthday" : { "$subtract" : [
{ "$add" : [
"$dayOfYear",
{ "$cond" : [{"$lt":["$dayOfYear","$todayDayOfYear"]},365,0 ] }
] },
"$todayDayOfYear"
] }
} };
排除所有范围内的所有内容:
var m = { "$match" : { "daysTillBirthday" : { "$lt" : 31 } } };
现在使用以下命令运行聚合:
db.collection.aggregate( p1, p2, m );
获取生日在30天内的所有幸运者的名单,生日和生日之前的清单。
修改的
var p1 = { "$project" : {
"_id" : 0,
"name" : 1,
"birthday" : 1,
"todayDayOfYear" : { "$dayOfYear" : ISODate("2014-03-09T12:30:51.515Z") },
"leap" : { "$or" : [
{ "$eq" : [ 0, { "$mod" : [ { "$year" : "$birthday" }, 400 ] } ] },
{ "$and" : [
{ "$eq" : [ 0, { "$mod" : [ { "$year" : "$birthday" }, 4 ] } ] },
{ "$ne" : [ 0, { "$mod" : [ { "$year" : "$birthday" }, 100 ] } ] } ] } ] },
"dayOfYear" : { "$dayOfYear" : "$birthday" } } };
var p1p = { "$project" : {
"name" : 1,
"birthday" : 1,
"todayDayOfYear" : 1,
"dayOfYear" : { "$subtract" : [
"$dayOfYear",
{ "$cond" : [ { "$and" : [ "$leap", { "$gt" : [ "$dayOfYear", 59 ] } ] }, 1, 0 ] } ] }
}
}
p2
和m
保持与上述相同。
测试输入:
db.birthdays.find({},{name:1,birthday:1,_id:0})
{ "name" : "Ally", "birthday" : ISODate("1975-06-12T00:00:00Z") }
{ "name" : "Ben", "birthday" : ISODate("1968-04-03T00:00:00Z") }
{ "name" : "Mark", "birthday" : ISODate("1949-12-23T00:00:00Z") }
{ "name" : "Paul", "birthday" : ISODate("2014-03-04T15:59:05.374Z") }
{ "name" : "Paul", "birthday" : ISODate("2011-02-07T00:00:00Z") }
{ "name" : "Sean", "birthday" : ISODate("2004-01-31T00:00:00Z") }
{ "name" : "Tim", "birthday" : ISODate("2008-02-28T00:00:00Z") }
{ "name" : "Sandy", "birthday" : ISODate("2005-01-31T00:00:00Z") }
{ "name" : "Toni", "birthday" : ISODate("2009-02-28T00:00:00Z") }
{ "name" : "Sam", "birthday" : ISODate("2005-03-31T00:00:00Z") }
{ "name" : "Max", "birthday" : ISODate("2004-03-31T00:00:00Z") }
{ "name" : "Jen", "birthday" : ISODate("1971-04-03T00:00:00Z") }
{ "name" : "Ellen", "birthday" : ISODate("1996-02-28T00:00:00Z") }
{ "name" : "Fanny", "birthday" : ISODate("1996-02-29T00:00:00Z") }
{ "name" : "Gene", "birthday" : ISODate("1996-03-01T00:00:00Z") }
{ "name" : "Edgar", "birthday" : ISODate("1997-02-28T00:00:00Z") }
{ "name" : "George", "birthday" : ISODate("1997-03-01T00:00:00Z") }
输出:
db.birthdays.aggregate( p1, p1p, p2, {$sort:{daysTillBirthday:1}});
{ "name" : "Sam", "birthday" : ISODate("2005-03-31T00:00:00Z"), "daysTillBirthday" : 22 }
{ "name" : "Max", "birthday" : ISODate("2004-03-31T00:00:00Z"), "daysTillBirthday" : 22 }
{ "name" : "Ben", "birthday" : ISODate("1968-04-03T00:00:00Z"), "daysTillBirthday" : 25 }
{ "name" : "Jen", "birthday" : ISODate("1971-04-03T00:00:00Z"), "daysTillBirthday" : 25 }
{ "name" : "Ally", "birthday" : ISODate("1975-06-12T00:00:00Z"), "daysTillBirthday" : 95 }
{ "name" : "Mark", "birthday" : ISODate("1949-12-23T00:00:00Z"), "daysTillBirthday" : 289 }
{ "name" : "Sean", "birthday" : ISODate("2004-01-31T00:00:00Z"), "daysTillBirthday" : 328 }
{ "name" : "Sandy", "birthday" : ISODate("2005-01-31T00:00:00Z"), "daysTillBirthday" : 328 }
{ "name" : "Paul", "birthday" : ISODate("2011-02-07T00:00:00Z"), "daysTillBirthday" : 335 }
{ "name" : "Tim", "birthday" : ISODate("2008-02-28T00:00:00Z"), "daysTillBirthday" : 356 }
{ "name" : "Toni", "birthday" : ISODate("2009-02-28T00:00:00Z"), "daysTillBirthday" : 356 }
{ "name" : "Ellen", "birthday" : ISODate("1996-02-28T00:00:00Z"), "daysTillBirthday" : 356 }
{ "name" : "Fanny", "birthday" : ISODate("1996-02-29T00:00:00Z"), "daysTillBirthday" : 356 }
{ "name" : "Edgar", "birthday" : ISODate("1997-02-28T00:00:00Z"), "daysTillBirthday" : 356 }
{ "name" : "Gene", "birthday" : ISODate("1996-03-01T00:00:00Z"), "daysTillBirthday" : 357 }
{ "name" : "George", "birthday" : ISODate("1997-03-01T00:00:00Z"), "daysTillBirthday" : 357 }
{ "name" : "Paul", "birthday" : ISODate("2014-03-04T15:59:05.374Z"), "daysTillBirthday" : 360 }
你可以看到,生日相同的人现在生日数相同,无论他们是否在闰年出生。现在可以对设计的截止点执行匹配步骤。
修改强>
从版本3.5.11开始,聚合管道中有几个日期操作表达式,这使得写入更加简单。特别是,$dateFromParts expression允许从各个部分构建日期,允许这种聚合:
var today = new Date();
var a1 = {$addFields:{
today:{$dateFromParts:{year:{$year:today},month:{$month:today},day:{$dayOfMonth:today}}},
birthdayThisYear:{$dateFromParts:{year:{$year:today}, month:{$month:"$birthday"}, day:{$dayOfMonth:"$birthday"}}},
birthdayNextYear:{$dateFromParts:{year:{$add:[1,{$year:today}]}, month:{$month:"$birthday"}, day:{$dayOfMonth:"$birthday"}}}
}};
var a2 = {$addFields:{
nextBirthday:{$cond:[ {$gte:[ "$birthdayThisYear", "$today"]}, "$birthdayThisYear", "$birthdayNextYear"]}
}};
var p1 = {$project:{
name:1,
birthday:1,
daysTillNextBirthday:{$divide:[
{$subtract:["$nextBirthday", "$today"]},
24*60*60*1000 /* milliseconds in a day */
]},
_id:0
}};
var s1 = {$sort:{daysTillNextBirthday:1}};
db.birthdays.aggregate([ a1, a2, p1, s1 ]);
您可以将“今天”设置为任何日期(闰年与否),并查看计算现在始终正确且更简单。
答案 1 :(得分:3)
明确的事情是出生日期是出生日期并且是过去的日期,但是我们想在将来搜索吗?是的好陷阱。
但我们可以通过aggregation中的一些预测来解决一种解决方法。
首先对我们需要的变量进行一些设置:
var start_time = new Date(),
end_time = new Date();
end_time.setDate(end_time.getDate() + 30 );
var monthRange = [ start_time.getMonth() + 1, end_time.getMonth() + 1 ];
var start_string = start_time.getFullYear().toString() +
("0" + (start_time.getMonth()+1)).slice(-2) +
("0" + (start_time.getDate()-1)).slice(-2);
var end_string = end_time.getFullYear().toString() +
("0" + (end_time.getMonth()+1)).slice(-2) +
("0" + (end_time.getDate()-1)).slice(-2);
var start_year = start_time.getFullYear();
var end_year = end_time.getFullYear();
然后通过aggregate:
运行db.users.aggregate([
{"$project": {
"name": 1,
"birthdate": 1,
"matchYear": {"$concat":[
// Substituting the year into the current year
{"$substr":[{"$cond":[
{"$eq": [{"$month": "$birthdate"}, monthRange[0]]},
start_year,
// Being careful to see if we moved into the next year
{"$cond":[
{"$lt": monthRange},
start_year,
end_year
]}
]},0,4]},
{"$cond":[
{"$lt":[10, {"$month": "$birthdate"}]},
{"$substr":[{"$month": "$birthdate"},0,2]},
{"$concat":["0",{"$substr":[{"$month": "$birthdate"},0,2]}]}
]},
{"$cond":[
{"$lt":[10, {"$dayOfMonth": "$birthdate"}]},
{"$substr":[{"$dayOfMonth": "$birthdate"},0,2]},
{"$concat":["0",{"$substr":[{"$dayOfMonth": "$birthdate"},0,2]}]}
]}
]}
}},
// Small optimize for the match stage
{"sort": { "matchYear": 1}},
// And match on the range now that it's lexical
{"$match": { "matchYear": {"$gte": start_string, "$lte": end_string } }}
])
如果你的思维方式更好,我认为同样适用于mapReduce。但无论你以何种方式震撼它,结果只会产生true
或false
。但你可能只需要一个映射器,语法更清晰:
var mapFunction = function () {
var mDate = new Date( this.birthdate.valueOf() );
if ( mDate.getMonth() + 1 < monthRange[0] ) {
mDate.setFullYear(start_year);
} else if ( monthRange[0] < monthRange[1] ) {
mDate.setFullYear(start_year);
} else {
mDate.setFullYear(end_year);
}
var matched = (mDate >= start_time && mDate <= end_time);
var result = {
name: this.name,
birthdate: this.birthdate,
matchDate: mDate,
matched: matched
};
emit( this._id, result );
};
然后你会把它传递给mapReduce,拿起之前定义的所有变量:
db.users.mapReduce(
mapFunction,
function(){}, // reducer is not called
{
out: { inline: 1 },
scope: {
start_year: start_year,
end_year: end_year,
start_time: start_time,
end_time: end_time,
monthRange: monthRange
}
}
)
但实际上,至少将“出生月”存储在真实字段中作为用户记录的一部分。因为那样你就可以缩小比赛范围,而不是处理你的整个系列。只需在管道的开头添加额外的$ match:
{"$match": "birthMonth": {"$in": monthRange }}
文档中存在可以在将来节省磁盘抖动的字段。
应该工作的另一种形式就是将原始JavaScript投入到find中。这可以作为快捷方式完成,您不提供任何其他查询条件。但令人困惑的是,文档位于$where运算符下,与将JavaScript传递给$where基本相同。
但是,任何尝试都不会产生结果。因此其他方法。不确定是否有充分的理由或是否是一个错误。
无论如何,除了前一年的翻转测试之外,所有测试都是在这些文件上完成的。如果初始开始日期是“2014-03-03”,则不应出现一个结果。
{ "name" : "bill", "birthdate" : ISODate("1973-03-22T00:00:00Z") }
{ "name" : "fred", "birthdate" : ISODate("1974-04-17T00:00:00Z") }
{ "name" : "mary", "birthdate" : ISODate("1961-04-01T00:00:00Z") }
{ "name" : "wilma", "birthdate" : ISODate("1971-03-17T00:00:00Z") }
答案 2 :(得分:1)
解决方案是将函数传递给find
Mongo操作。请参阅内联评论:
// call find
db.users.find(function () {
// convert BSON to Date object
var bDate = new Date(this.birthday * 1000)
// get the present moment
, minDate = new Date()
// add 30 days from this moment (days, hours, minutes, seconds, ms)
, maxDate = new Date(minDate.getTime() + 30 * 24 * 60 * 60 * 1000);
// modify the year of the birthday Date object
bDate.setFullYear(minDate.getFullYear());
// return a boolean value
return (bDate > minDate && bDate < maxDate);
});
答案 3 :(得分:1)
我认为最优雅,通常最有效的解决方案是使用aggregation framework。要获得生日,我们需要放弃除$month和$dayOfMonth之外的所有日期信息。我们使用这些值创建新的复合字段,对它们进行旋转,然后我们离开!
此javascript可以从mongo控制台执行,并在名为users
的集合上运行,其字段名为birthday
。它返回按生日分组的用户ID列表。
var next30days = [];
var today = Date.now();
var oneday = (1000*60*60*24);
var in30days = Date.now() + (oneday*30);
// make an array of all the month/day combos for the next 30 days
for (var i=today;i<in30days;i=i+oneday) {
var thisday = new Date(i);
next30days.push({
"m": thisday.getMonth()+1,
"d": thisday.getDate()
});
}
var agg = db.users.aggregate([
{
'$project': {
"m": {"$month": "$birthday"},
"d": {"$dayOfMonth": "$birthday"}
}
},
{
"$match": {
"$or": next30days
}
},
{
"$group": {
"_id": {
"month": "$m",
"day": "$d",
},
"userids": {"$push":"$_id"}
}
}
]);
printjson(agg);