找一个人是否在接下来的30天里用mongo过生日

时间:2014-02-26 12:43:06

标签: mongodb aggregation-framework

假设我们收到了一组用户,每个用户都有BSON类型日期格式的生日。

我们如何运行查询以找出在接下来的30天内过生日的所有用户?

4 个答案:

答案 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天内的所有幸运者的名单,生日和生日之前的清单。

修改

@ Sean999发现了一个有趣的边缘案例 - 2月28日之后出生在闰年的人将把他们的计算结果减去一个。以下是正确调整的聚合:

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 ] } ] }
        }
}

p2m保持与上述相同。

测试输入:

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。但无论你以何种方式震撼它,结果只会产生truefalse。但你可能只需要一个映射器,语法更清晰:

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);