【问题标题】:Find whether someone got a birthday in the next 30 days with mongo使用 mongo 查找未来 30 天内是否有人过生日
【发布时间】:2014-03-29 07:18:31
【问题描述】:

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

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

【问题讨论】:

  • 这是一个家庭作业问题吗?我在几天内看到几个问题与这个问题非常相似......
  • 不,这不是功课,事实上,我正在建立一个电子商务网站,用优惠券代码向客户发送生日快乐,并询问他们是否想在生日前买东西.你知道,它会以某种方式增加收入。

标签: mongodb aggregation-framework


【解决方案1】:

解决方案是将函数传递给find Mongo 操作。查看内联 cmets:

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

【讨论】:

  • 这个应该可以工作,但我一辈子都做不到。我在提交的答案中写了一个注释,还显示了我正在测试的数据。是的,我确实更改了该字段的名称。另一部分是当月份是 12 月时,这会受到错误的影响。顺便说一句,第一个变量声明后缺少逗号。
  • @NeilLunn 我相信还有其他更有效的方法。我对此进行了测试,效果很好。没有缺少逗号。我把逗号放在下一行。请检查并测试我的代码。
  • 是的。我知道它应该。这就是我所做的。事实上,这是首先想到的。但我没有得到任何结果。
  • 而奇怪的是,采用完全相同的代码并提交给 mapReduce,将最后一行更改为发出。并且正确的真假计数出来了。所以因此。难住了。
  • 知道了。不需要 epoch 的 1000 乘法。至少从 2.4.8 及更高版本对我来说是这样。
【解决方案2】:

很明显,出生日期是出生日期并且是过去的日期,但是我们想在未来进行搜索,对吗?是的不错的陷阱

但我们可以通过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") }

【讨论】:

  • 您不应该在聚合框架中将日期强制转换为字符串(从技术上讲,您不应该被允许这样做,这是一个让它溜走的错误,所以它会在某个时候停止工作) .这也不是必需的,因为您可以将它们作为日期处理。
  • @AsyaKamsky 相信我,我宁愿整个事情都是约会。
  • 你可以做到 - 只需使用日期数学。我原本打算这样回答,但我发布的答案更短,更容易理解。
  • @AsyaKamsky 实际上我确实考虑过进行日期数学运算,但该解决方案似乎比主要在这里完成的 casting 更迟钝。但当然,这不是很好,我经常告诉人们不要使用字符串作为日期。不是重点,(因为我相信从一年中的那一天开始工作是最清楚的)但是说以这种方式从 Int 到 String 的 $substr 强制是一个错误,它已经存在了 long 时间,似乎被广泛使用。所以改变这种行为破坏很多人的代码。需要考虑的事情。
  • @NeilLunn +1 虽然我将我的赏金献给了 Asya Kamsky,但您的解释非常清楚和有帮助。谢谢。
【解决方案3】:

我认为最优雅且通常最有效的解决方案是使用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);

【讨论】:

  • 我喜欢这种通用方法,但我认为有一种更优雅的方法可以做到这一点,而无需预先计算整个 30 天范围的 d/m 对数组...
  • 我考虑过将其简化为范围查询,因此它不是一个 30 元素列表,而是看起来像 ` { "$or": [ {"month":2,"dayofmonth": {"$gte": 15}}, {"month":3,"dayofmonth": {"$lte": 14}} ] } `
  • 您实际上可以将生日生成为常规日期以进行范围查询,或者还有另一种我喜欢的方法 - 我会把它写下来作为我的答案......
【解决方案4】:

聚合框架绝对是正确的方法 - 任何需要在服务器上运行 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 ]);

您可以将“今天”设置为任何日期(闰年或非闰年),然后查看计算现在始终正确且简单得多。

【讨论】:

  • 当然,如果今年生日已经过,这就是为什么我将 365 添加到 dayOfYear 的原因。 (根据 dayOfYear 是否小于今天的 dayOfYear,我添加 0 或 365)
  • 这很优雅,但它假设每年有 365 天。
  • @sean9999 是的,非常优雅,(错过了它的个人脸)。对于大多数语言来说,确定闰年是微不足道的,所以只需将其传递给生成。 JavaScript 的一种解决方案。
  • 不,@sean9999 闰年没有什么不同,除非您试图找出谁在接下来的 60 多天过生日,然后只有在 1 月运行此查询时才重要或二月。老实说,试试看。闰年离年初还有30多天:)
  • 我想唯一要补充的是项目可能只有一个 $project 阶段。对于大多数语言来说,获取一年中的 当前 天是微不足道的,并且不需要单独的管道阶段只是为了针对所有文档投射相同的值。但正如已经评论的那样,优雅和干净可以解决基本问题。我已经投票了。
猜你喜欢
  • 2011-10-31
  • 1970-01-01
  • 1970-01-01
  • 2016-07-01
  • 1970-01-01
  • 2011-12-26
  • 2018-11-09
  • 2011-08-11
  • 1970-01-01
相关资源
最近更新 更多