查询数组中的最后匹配日期

时间:2019-03-01 15:06:39

标签: javascript mongodb

我需要在mongoDB中找到具有过期日期值的所有数据集。过期意味着最后一个数组元素的时间戳早于当前时间戳加上定义的间隔(由类别定义)

每个数据集都具有这样的field

{
  "field" : [
      {
        "category" : 1,
        "date" : ISODate("2019-03-01T12:00:00.464Z")
      },
      {
        "category" : 1,
        "date" : ISODate("2019-03-01T14:52:50.464Z")
      }
    ]
}

类别定义时间间隔。例如,“类别1”代表90分钟,“类别2”代表120分钟。

现在我需要为每个数据集获取一个过期的日期值,这意味着最后一个数组元素的值比当前时间戳记早90分钟。

类似

Content.find({ 'field.$.date': { $gt: new Date() } })

但是通过这种尝试,我遇到了两个问题:

  1. 如何查询最后一个数组元素?
  2. 如何在查询中实现类别时间间隔?

1 个答案:

答案 0 :(得分:1)

让我们将问题分成几部分。

查询“最后一个”(最近的)数组元素

第1部分:逻辑快速

快速浏览MongoDB query operators related to arrays应该告诉您,实际上您总是可以根据索引位置查询数组元素。这对于“第一个”数组元素非常简单,因为该位置始终为0

{ "field.0.date": { "$lt": new Date("2019-03-01T10:30:00.464Z") } }

从逻辑上讲,“最后”位置是-1,但是您实际上不能在MongoDB中以这种形式的符号使用该值,因为它将被视为无效。

不过,您可以在此处执行的操作是以某种方式将新项目添加到数组中,而不是追加 end 数组,您实际上是开头到数组的开始。这意味着您的数组内容实际上是“ reversed” ,然后可以轻松访问,如上所示。这是$position$push修饰符为您所做的:

collection.updateOne(
  { "_id": documentId },
  {
    "$push": {
      "field": { 
        "$each": [{ "category": 1, "date": new Date("2019-03-02") }],
        "$position": 0
      }
    }
  }
 )

因此,这意味着新添加的项目应放在开头而不是结尾。这可能很实用,但这确实意味着您需要重新排序所有现有的数组项。

如果"date" static 且一旦写入数组项基本上就不会改变(即,您从未更新匹配的数组项的日期),则实际上使用$sort修饰符在单个update语句中对该"date"属性进行排序:

collection.updateMany(
  {},
  { "$push": { "field": { "$each": [], "$sort": { "date": -1 } } } }
)

虽然实际上没有向数组中添加任何内容时使用$push可能会感觉很奇怪,但这是$sort修饰符的存在之处。空数组"$each": []参数实际上意味着“什么也不添加” ,而$sort适用于该数组的所有当前成员。

可以选择执行此操作,就像使用$position的先前示例一样,其中$sort将应用于每次写入。但是,只要"date"适用于“添加时的时间戳” (我怀疑确实如此),那么使用"$position": 0方法而不是对每个对象进行排序可能更有效时间有些变化。取决于您的实际实现以及如何处理数据。

第2部分:蛮力和缓慢

但是,如果出于某种原因您真的不相信能够“反向” 数组的内容是切实可行的解决方案,那么唯一可行的方法就是有效地通过从受支持的运算符投影此值来“计算” 数组元素。

唯一可行的方法通常是使用Aggregation Framework,特别是$arrayElemAt运算符:

collection.aggregate([
  { "$addFields": {
    "lastDate": { "$arrayElemAt": [ "$field.date", -1 ] }
  }}
])

基本上,这只是看提供的数组内容(在这种情况下,只是每个元素的"date"属性值),然后在给定的索引位置提取值。该运算符允许使用-1索引符号,表示数组中的“最后一个”元素。

显然,这不是理想的选择,因为提取 query filter 所需的实际表达式分离。在下一部分中,但是您需要在整个集合中意识到这一点,然后我们才能查看比较值以查看要保留的值。

按“类别”采样日期

第1部分:快速查询逻辑

根据上述内容,下一个条件基于"category"字段值,下一个主要问题是

  • 90分钟调整为值1
  • 120分钟调整为值2

根据刚刚学到的相同逻辑,您应该得出结论,在处理数据时“计算” 对于性能而言是“坏消息” 。因此,此处要应用的技巧基本上包括查询表达式中的逻辑,以便根据"date"的值使用不同的提供的"category"值在文档中匹配。

最简单的应用是使用$or表达式:

var currentDateTime = new Date();

var ninetyMinsBefore = new Date(currentDateTime.valueOf() - (1000 * 60 * 90));
var oneTwentyMinsBefore = new Date(currentDateTime.valueOf() - (1000 * 60 * 120));


collection.find({
  "$or": [
    {
      "field.0.category": 1,
      "field.0.date": { "$lt": ninetyMinsBefore }
    },
    {
      "field.0.category": 2,
      "field.0.date": { "$lt": oneTwentyMinsBefore }
    }
  ]
 })

请注意,与其计算由可变间隔调整后的"date",而不是查看与当前日期的比较,而是计算与当前日期的差,然后根据值有条件地应用的"category"

这是一种快速而有效的方法,因为您能够如上所述对数组项进行重新排序,然后我们可以应用条件来查看“ first” 元素是否满足它们。 / p>

第2部分:强制计算速度较慢

collection.aggregate([
  { "$addFields": {
    "lastDate": { 
      "$arrayElemAt": [ "$field.date", -1 ]
    },
    "lastCategory": {
      "$arrayElemAt": [ "$field.category", -1 ]
    }
  }},
  { "$match": {
    "$or": [
      { "lastCategory": 1, "lastDate": { "$lt": ninetyMinsBefore } },
      { "lastCategory": 2, "lastDate": { "$lt": oneTwentyMinsBefore } }
    ]
  }}
])

具有相同的基本前提,即使您已经需要从“ last” 数组元素中投影值,也没有必要真正用数学来调整存储的"date"值,这将是事情变得更加复杂。

最初的$addFields预测是主要成本,因此主要的不利因素是底部的$match

您可以选择将$expr与现代MongoDB版本一起使用,但这基本上是相同的:

collection.find({
  "$expr": {
    "$or": [
      {
        "$and": [
          { "$eq": [ { "$arrayElemAt": [ "$field.category", -1 ] }, 1 ] },
          { "$lt": [ { "$arrayElemAt": [ "$field.date", -1 ] }, ninetyMinsBefore ] }
        ]
      },
      {
        "$and": [
          { "$eq": [ { "$arrayElemAt": [ "$field.category", -1 ] }, 2 ] },
          { "$lt": [ { "$arrayElemAt": [ "$field.date", -1 ] }, oneTwentyMinsBefore ] }
        ]
      }
    ]
  }
})

值得一提的是$or$and的特殊“聚集”形式,因为$expr中的所有内容都是聚集表达式,需要解析为{{ 1}}的值为Boolean

无论哪种方式,都与最初的“仅查询”示例一样,都是本地处理的问题,实际上可以使用索引来加快匹配和结果的速度。这些“聚合表达式”都不能做到这一点,因此运行起来相当慢。


  

注意::如果要存储true/false的目的是将“ expired”作为要选择的日期,则表示该日期小于当前日期(减去时间间隔),而不是您在问题中提出的“大于”。

     

这表示当前时间,然后减去间隔(而不是添加到存储的时间)将是所选内容中的“更大”值,因此该时间之前 过期。

     

NB 通常,当您查询array elements with documents matching multiple properties时,您将使用$elemMatch运算符,以便将多个条件应用于该特定于 的数组元素。

     

此处仅适用 的原因是因为在"date"位置使用了数字索引值明确地在每个属性上。这意味着,它不是在整个数组(如0)上而是专门应用于 "field.date"位置。