【问题标题】:How to calculate difference of lowest date and highest date among array of sub documents?如何计算子文档数组中最低日期和最高日期的差异?
【发布时间】:2018-04-19 17:32:16
【问题描述】:

我有以下子文档:

experiences: [
{
    "workExperienceId" : ObjectId("59f8064e68d1f61441bec94a"),
    "workType" : "Full Time",
    "functionalArea" : "Law",
    "company" : "Company A",
    "title" : "new",
    "from" : ISODate("2010-10-13T00:00:00.000Z"),
    "to" : ISODate("2012-10-13T00:00:00.000Z"),
    "_id" : ObjectId("59f8064e68d1f61441bec94b"),
    "currentlyWorking" : false
},
...
...
{
    "workExperienceId" : ObjectId("59f8064e68d1f61441bec94a"),
    "workType" : "Full Time",
    "functionalArea" : "Law",
    "company" : "Company A",
    "title" : "new",
    "from" : ISODate("2014-10-14T00:00:00.000Z"),
    "to" : ISODate("2015-12-13T00:00:00.000Z"),
    "_id" : ObjectId("59f8064e68d1f61441bec94c"),
    "currentlyWorking" : false
},
{
    "workExperienceId" : ObjectId("59f8064e68d1f61441bec94a"),
    "workType" : "Full Time",
    "functionalArea" : "Law",
    "company" : "Company A",
    "title" : "new",
    "from" : ISODate("2017-10-13T00:00:00.000Z"),
    "to" : null,
    "_id" : ObjectId("59f8064e68d1f61441bec94d"),
    "currentlyWorking" : true
},
{
    "workExperienceId" : ObjectId("59f8064e68d1f61441bec94a"),
    "workType" : "Full Time",
    "functionalArea" : "Law",
    "company" : "Company A",
    "title" : "new",
    "from" : ISODate("2008-10-14T00:00:00.000Z"),
    "to" : ISODate("2009-12-13T00:00:00.000Z"),
    "_id" : ObjectId("59f8064e68d1f61441bec94c"),
    "currentlyWorking" : false
},
]

如您所见,在连续日期内可能没有排序日期,也可能是未排序日期。以上数据是针对每个用户的。所以我想要的是以年份格式为每个用户获得总年数的经验。当to 字段为空且currentlyWorking 为真时,则表示我目前在该公司工作。

【问题讨论】:

    标签: javascript mongodb datetime mongoose aggregation-framework


    【解决方案1】:

    聚合

    使用聚合框架,您可以将$indexOfArray 应用到可用的位置:

    Model.aggregate([
      { "$addFields": {
        "difference": {
          "$subtract": [
            { "$cond": [
                { "$eq": [{ "$indexOfArray": ["$experiences.to", null] }, -1] },
                { "$max": "$experiences.to" },
                new Date()
            ]},
            { "$min": "$experiences.from" }
          ]
        }
      }}
    ])
    

    如果“最新”始终是数组中的最后一个,则使用$arrayElemAt

    Model.aggregate([
      { "$addFields": {
        "difference": {
          "$subtract": [
            { "$cond": [
                { "$eq": [{ "$arrayElemAt": ["$experiences.to", -1] }, null] },
                new Date(),
                { "$max": "$experiences.to" }
            ]},
            { "$min": "$experiences.from" }
          ]
        }
      }}
    ])
    

    这几乎是最有效的方法,作为应用$min$max 运算符的单个管道阶段。对于$indexOfArray,您至少需要 MongoDB 3.4,而对于简单地使用 $arrayElemAt,您可以使用 MongoDB 3.2,这是您应该在生产环境中运行的最低版本。

    一次通过,意味着它可以以很少的开销快速完成。

    简短的部分是$min$max 允许您直接从数组元素中提取适当的值,即数组中"from" 的“最小”值和"to" 的最大值。在可用的情况下,$indexOfArray 运算符可以从提供的数组(在本例中为 "to" 值)返回匹配的索引,其中存在指定的值(如null 此处)。如果存在则返回该值的索引,如果不存在则返回 -1 的值,表示未找到。

    我们使用$cond 这是一个“三元”或if..then..else 运算符来确定当找不到null then 时,您需要来自"to"$max 值。当然,当它被发现是 else 时,会返回当前 Date 的值,该值作为执行时的外部参数输入聚合管道。

    MongoDB 3.2 的另一种情况是,您改为“假定”数组的最后一个元素是最近的就业历史项目。通常最好的做法是订购这些项目,所以最近的是“最后一个”(如您的问题中所示)或数组的“第一个”条目。保持这些条目的顺序是合乎逻辑的,而不是依赖于在运行时对列表进行排序。

    所以当使用“已知”位置如“last”时,我们可以使用$arrayElemAt 运算符从数组中返回指定位置的值。这里是“最后一个”元素的-1。 “第一个”元素将是 0,并且可以说也可以应用于获取 "from" 的“最小”值,因为您应该按顺序排列数组。同样$cond 用于根据是否返回null 来转置值。作为$max 的替代品,您甚至可以使用$ifNull 来交换值:

    Model.aggregate([
      { "$addFields": {
        "difference": {
          "$subtract": [
            { "$ifNull": [{ "$arrayElemAt": ["$experiences.to", -1] }, new Date()] },
            { "$min": "$experiences.from" }
          ]
        }
      }}
    ])
    

    如果第一个条件的响应是null,则该运算符实质上会切换返回的值。因此,由于我们已经从“最后一个”元素中获取值,我们可以“假设”这确实意味着 "to" 的“最大”值。

    $subtract 是实际返回“差异”的原因,因为当您从另一个日期“减去”一个日期时,差异将作为两者之间的毫秒值返回。这就是 BSON 日期在内部实际存储的方式,它是日期格式的常见内部日期存储,即“自纪元以来的毫秒数”。

    如果您想要特定持续时间的间隔,例如“年”,那么应用“日期数学”来更改日期值之间的 毫秒 差异是一件简单的事情。因此,通过从间隔中除以进行调整(为了完整起见,还在"from" 上显示$arrayElemAt):

    Model.aggregate([
      { "$addFields": {
        "difference": {
          "$floor": {
            "$divide": [
              { "$subtract": [
                { "$ifNull": [{ "$arrayElemAt": ["$experiences.to", -1] }, new Date()] },
                { "$arrayElemAt": ["$experiences.from", 0] }
              ]},
              1000 * 60 * 60 * 24 * 365
            ]
          }
        }
      }}
    ])
    

    这使用$divide 作为数学运算符,1000 毫秒60 用于秒和分钟,24 小时和365 天作为除数值。 $floor 从小数位“向下舍入”数字。你可以在那里做任何你想做的事情,但它“应该”“内联”使用,而不是在单独的阶段,这只会增加处理开销。

    当然,365 天的假设充其量只是一个“近似值”。如果您想要更完整的内容,则可以改为将date aggregation operators 应用于值以获得更准确的读数。所以在这里,也应用$let 声明为“变量”以供以后操作:

    Model.aggregate([
      { "$addFields": {
        "difference": {
          "$let": {
            "vars": {
              "to": { "$ifNull": [{ "$arrayElemAt": ["$experiences.to", -1] }, new Date()] },
              "from": { "$arrayElemAt": ["$experiences.from", 0] }
            },
            "in": {
              "years": { 
                "$subtract": [
                  { "$subtract": [
                    { "$year": "$$to" }, 
                    { "$year": "$$from" }
                  ]},
                  { "$cond": {
                    "if": { "$gt": [{ "$month": "$$to" },{ "$month": "$$from" }] },
                    "then": 0,
                    "else": 1
                  }}
                ]
              },
              "months": {
                "$add": [
                  { "$subtract": [
                    { "$month": "$$to" },
                    { "$month": "$$from" }
                  ]},
                  { "$cond": {
                    "if": { "$gt": [{ "$month": "$$to" },{ "$month": "$$from" }] },
                    "then": 0,
                    "else": 12
                  }}
                ]
              },
              "days": {
                "$add": [
                  { "$subtract": [
                    { "$dayOfYear": "$$to" },
                    { "$dayOfYear": "$$from" }
                  ]},
                  { "$cond": {
                    "if": { "$gt": [{ "$month": "$$to" },{ "$month": "$$from" }] },
                    "then": 0,
                    "else": 365
                  }}
                ]
              }
            }
          }
        }
      }}
    ])
    

    这又是一年中日期的轻微近似值。 MongoDB 3.6 实际上允许您通过实现 $dateFromParts 来测试“闰年”,以确定 2 月 29 日在当年是否有效,方法是从我们可用的“部分”组装。


    使用返回的数据

    当然,以上所有内容都是使用聚合框架来确定每个人的数组的间隔。如果您打算通过根本不返回数组项来“减少”返回的数据,或者如果您希望这些数字进一步聚合以报告更大的“总和”或“平均”统计数据,这将是建议的课程数据。

    另一方面,如果您确实希望为该人返回所有数据,包括完整的“经验”数组,那么最好的做法可能是在从服务器在您处理每个返回的项目时。

    此方法的简单应用是将新字段“合并”到结果中,就像 $addFields 所做的那样,但在“客户端”端:

    Model.find().lean().cursor().map( doc => 
      Object.assign(doc, {
        "difference": 
          ((doc.experiences.map(e => e.to).indexOf(null) === -1)
            ? Math.max.apply(null, doc.experiences.map(e => e.to))
            : new Date() )
          - Math.min.apply(null, doc.experiences.map(e => e.from)
      })
    ).toArray((err, result) => {
       // do something with result
    })
    

    这只是将第一个聚合示例中表示的相同逻辑应用于结果游标的“客户端”处理。由于您使用的是 mongoose,.cursor() 方法实际上从底层驱动程序返回了一个 Cursor 对象,为了“方便”,mongoose 通常会将其隐藏起来。在这里我们想要它,因为它让我们可以访问一些方便的方法。

    Cursor.map() 是一种方便的方法,它允许对从服务器返回的内容应用“转换”。这里我们使用Object.assign() 将新属性“合并”到返回的文档中。我们可以交替使用 Array.map() 对 mongoose “默认”返回的“数组”,但内联处理看起来更简洁,也更高效。

    事实上Array.map() 是这里操作的主要工具,因为我们在聚合语句中应用了"$experiences.to" 之类的语句,我们使用doc.experiences.map(e => e.to) 应用在“客户端”上,它执行相同的操作“转换”将对象数组转换为指定字段的“值数组”。

    这允许使用Array.indexOf() 对值数组进行相同的检查,并且Math.min()Math.max() 也以相同的方式使用,实现apply() 以使用这些“映射”数组值作为参数函数的值。

    当然,由于我们仍然返回了 Cursor,因此我们将其转换回更典型的形式,您可以使用 Cursor.toArray() 将 mongoose 结果作为“数组”处理,这正是 mongoose 在“下”所做的引擎盖”为您提供默认请求。

    Query.lean() 是一个 mongoose 修饰符,它基本上表示返回并期望“普通 JavaScript 对象”,而不是与模式匹配的“mongoose 文档”,应用方法又是默认返回。我们想要这样,因为我们正在“操纵”结果。另一种方法是在“返回”默认数组“之后”进行操作,并通过所有 mongoose 文档中存在的.toObject() 进行转换,以防“序列化虚拟属性”对您很重要。

    所以这本质上是第一种聚合方法的“镜像”,但适用于“客户端”逻辑。如前所述,当您实际上希望文档中的所有属性无论如何都包含在结果中时,这样做通常更有意义。原因很简单,因为在从服务器返回结果“之前”返回的结果中添加“附加”数据是没有意义的。因此,只需在数据库返回它们“之后”应用转换即可。

    与上述非常相似,可以应用与所有聚合示例中演示的相同的客户端转换方法。您甚至可以使用外部库进行日期操作,这些库为您提供了一些“原始数学”方法的“帮助”。

    【讨论】:

    • 困惑,您的查询返回与@Felix 查询不同的结果。现在不知道哪个返回正确的结果。
    • @NimatullahRazmjo 不知道其他人已经回答了。你想得到什么不同?我给你“每个文档内”的数组的区别。那么你到底在寻找什么?我认为这是一个人的“工作经历”。因此,在“人”(这将是一个文档)范围之外查看“来自”和“至”似乎没有任何意义。
    • 想知道用户的总经验。
    • @NimatullahRazmjo 当然,“用户”是一个文档对吗?这里的“数组”代表了他们列出的所有“经验”或“工作”,从头到尾。这就是我要给你的,是数组中的$min 与数组中的$max 之间的区别,或者当然你有一个null 是“今天的日期”。返回的时间间隔是日期之间的毫秒数。但是获得年份是基本的日期数学。所以这就是你要求的。查看其他答案,不需要所有其他阶段。而$unwind 让它变得非常缓慢。
    • @NimatullahRazmjo 您仍然对差异感到困惑(坦率地说我需要睡眠),但简短的情况是 $unwind 是处理数组值的 old 方法,它是对于您在问题中概述的目的并不是很有效。因此,虽然您“可以”使用这种方法,但它确实会付出巨大的代价,而这可以通过以更有效的方式从数组中提取值来避免。添加了对此方法的完整概述以及为什么要以这种方式使用它们。
    【解决方案2】:

    您可以使用这样的聚合框架来实现这一点:

    db.collection.aggregate([
       {
          $unwind:"$experiences"
       },
       {
          $sort:{
             "experiences.from":1
          }
       },
       {
          $group:{
             _id:null,
             "from":{
                $first:"$experiences.from"
             },
             "to":{
                $last:{
                   $ifNull:[
                      "$to",
                      new Date()
                   ]
                }
             }
          }
       },
       {
          $project:{
             "diff":{
                $subtract:[
                   "$to",
                   "$from"
                ]
             }
          }
       }
    ])
    

    这会返回:

    { "_id" : null, "diff" : NumberLong("65357827142") }
    

    两个日期之间的 ms 相差多少,详见$subtract

    您可以通过将此附加阶段添加到管道末尾来获得 年份

    {
       $project:{
          "year":{
             $floor:{
                $divide:[
                   "$diff",
                   1000*60*60*24*365
                ]
             }
          }
       }
    }
    

    这将返回:

    { "_id" : null, "year" : 2 }
    

    【讨论】:

    • 我们如何计算它的年或月差异,有没有像mysql这样的函数YEARMONTH或者我们应该自己计算。
    • 这在其他一些聚合阶段是可能的,但是直接在javascript中使用moment.js之类的库会更容易。
    • 但是我在查询中需要这个,因为我有一个年份值,并且想将这个差异与那个年份值进行比较。
    • 谢谢 好像没问题,让我检查一下。但为了让你更清楚,我想知道每个用户的总经验年限。
    • 您的答案仅针对一位用户返回差异。
    猜你喜欢
    • 2012-12-22
    • 1970-01-01
    • 1970-01-01
    • 2022-01-10
    • 1970-01-01
    • 2014-12-31
    • 2020-11-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多