查找所有重叠间隔的计数

时间:2018-04-24 15:47:23

标签: mongodb mongodb-query aggregation-framework

我有这样的所有记录的startTime和endTime:

{
 startTime : 21345678
 endTime   : 31345678
}

我试图找到所有冲突的数量。例如,如果有两个记录并且它们重叠,则冲突的数量为1.如果有三个记录并且其中两个重叠,则冲突为1.如果有三个记录且所有三个重叠,则冲突为3,即{{1} }

作为一种算法,我正在考虑按开始时间对数据进行排序,并为每个已排序的记录检查结束时间,并查找开始时间小于结束时间的记录。这将是O(n 2 )时间。更好的方法是使用区间树并将每条记录插入树中,并在发生重叠时查找计数。这将是O(nlgn)时间。

我没有使用过mongoDB,所以我可以用什么样的查询来实现这样的目标?

1 个答案:

答案 0 :(得分:2)

正如您所正确提到的,有不同的方法,其执行固有的复杂性各不相同。这基本上涵盖了它们的完成方式以及实现的方式实际取决于您的数据和用例最适合的方式。

当前范围匹配

MongoDB 3.6 $ lookup

使用MongoDB 3.6的$lookup运算符的新语法可以采用最简单的方法,允许将pipeline作为表达式给予" self join"到同一个集合。这基本上可以再次查询集合starttime"或"当前文档的endtime介于任何其他文档的相同值之间,当然不包括原文:

db.getCollection('collection').aggregate([
  { "$lookup": {
    "from": "collection",
    "let": {
      "_id": "$_id",
      "starttime": "$starttime",
      "endtime": "$endtime"
    },
    "pipeline": [
      { "$match": {
        "$expr": {
          "$and": [
            { "$ne": [ "$$_id", "$_id" },
            { "$or": [
              { "$and": [
                { "$gte": [ "$$starttime", "$starttime" ] },
                { "$lte": [ "$$starttime", "$endtime" ] }
              ]},
              { "$and": [
                { "$gte": [ "$$endtime", "$starttime" ] },
                { "$lte": [ "$$endtime", "$endtime" ] }
              ]}
            ]},
          ]
        },
        "as": "overlaps"
      }},
      { "$count": "count" },
    ]
  }},
  { "$match": { "overlaps.0": { "$exists": true }  } }
])

$lookup执行"加入"在同一个集合中,您可以保留当前文档"分别通过管道阶段的"_id"选项的"starttime""endtime""let"值的值。这些将作为"局部变量"在表达式的后续$$中使用"pipeline"前缀。

在这个"子管道中#34;您使用$match管道阶段和$expr查询运算符,它允许您将聚合框架逻辑表达式作为查询条件的一部分进行评估。这允许在选择符合条件的新文档时比较值。

条件只是寻找"处理过的文件" "_id"字段不等于"当前文档",$and其中"starttime"  "当前文档"的$or "endtime"值介于"处理过的文档"的相同属性之间。请注意,这些以及相应的$gte$lte运算符是"aggregation comparison operators"而不是"query operator"形式,因为$expr评估的返回结果必须在上下文中boolean。这就是聚合比较运算符实际执行的操作,也是传递值进行比较的唯一方法。

因为我们只想要"计数"在匹配中,$count管道阶段用于执行此操作。总体$lookup的结果将是"单个元素"存在计数的数组,或者#34;空数组"那里的条件不匹配。

另一种情况是"省略" $count阶段,只需允许匹配的文档返回。这样可以轻松识别,但作为嵌入文档中的"数组"你需要注意"重叠"的数量。这将作为整个文件返回,并且这不会导致违反16MB的BSON限制。在大多数情况下,这应该没问题,但是对于您希望给定文档有大量重叠的情况,这可能是一个真实的情况。所以它真的需要注意的事情。

此上下文中的$lookup管道阶段将始终"始终"返回结果中的数组,即使是空的。输出属性的名称"合并"进入现有文档的"overlaps"属性"as"属于$lookup阶段。{/ p>

$lookup之后,我们可以使用常规查询表达式执行简单的$match,该表达式使用$exists测试输出数组的0索引值。数组中实际存在某些内容,因此"重叠"条件为真,文件返回,显示计数或文件"重叠"根据你的选择。

其他版本 - 查询"加入"

您的MongoDB缺少此支持的另一种情况是"加入"通过为每个检查的文档发出上述相同的查询条件来手动:

db.getCollection('collection').find().map( d => {
  var overlaps = db.getCollection('collection').find({
    "_id": { "$ne": d._id },
    "$or": [
      { "starttime": { "$gte": d.starttime, "$lte": d.endtime } },
      { "endtime": { "$gte": d.starttime, "$lte": d.endtime } }
    ]
  }).toArray();

  return ( overlaps.length !== 0 ) 
    ? Object.assign(
        d,
        {
          "overlaps": {
            "count": overlaps.length,
            "documents": overlaps
          }
        }
      )
    : null;
}).filter(e => e != null);

这基本上是相同的逻辑,除了我们实际上需要回到数据库"为了发出查询以匹配重叠的文档。这次它是"查询运算符"用于查找当前文档值在处理文档的值之间的位置。

由于结果已从服务器返回,因此在向输出添加内容时没有BSON限制限制。您可能有内存限制,但这是另一个问题。简单地说,我们通过.toArray()返回数组而不是光标,因此我们有匹配的文档,只需访问数组长度即可获得计数。如果您实际上不需要这些文档,那么使用.count()代替.find()会更有效率,因为没有文档获取开销。

然后输出简单地与现有文档合并,其中另一个重要的区别是,因为这些是"多个查询"没有办法提供他们必须“匹配”的条件。一些东西。因此,我们考虑将会有结果,其中计数(或数组长度)为0,此时我们所能做的就是返回null值,我们可以稍后.filter()来自结果数组。迭代光标的其他方法采用相同的基本原理"丢弃"结果我们不想要它们。但是没有什么能阻止在服务器上运行查询,而且这个过滤是"后期处理"以某种形式或其他形式。

降低复杂性

因此,上述方法适用于所描述的结构,但当然总体复杂性要求对于每个文档,您必须基本上检查集合中的每个其他文档以查找重叠。因此,虽然使用$lookup允许一些"效率" 减少传输和响应开销,但它仍然遇到同样的问题,您仍然主要是将每个文档与所有文档进行比较

更好的解决方案&#34;您可以使其适合<#34; 而是存储&#34;硬值&#34; *代表每个文档的间隔。例如,我们可以假设&#34;有坚实的预订&#34;一天中有一小时的时段,总共24个预订期。这&#34;可以&#34;表示如下:

{ "_id": "A", "booking": [ 10, 11, 12 ] }
{ "_id": "B", "booking": [ 12, 13, 14 ] }
{ "_id": "C", "booking": [ 7, 8 ] }
{ "_id": "D", "booking": [ 9, 10, 11 ] }

如果组织的数据类似于间隔的设定指标,那么复杂性会大大降低,因为它实际上只是一个问题,即分组&#34;来自"booking"属性中数组的间隔值:

db.booking.aggregate([
  { "$unwind": "$booking" },
  { "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
  { "$match": { "docs.1": { "$exists": true } } }
])

输出:

{ "_id" : 10, "docs" : [ "A", "D" ] }
{ "_id" : 11, "docs" : [ "A", "D" ] }
{ "_id" : 12, "docs" : [ "A", "B" ] }

这正确地标识了1011时间间隔,"A""D"包含重叠,而"B""A"重叠在12。其他间隔和文档匹配通过相同的$exists测试排除,除了这次1索引(或第二个数组元素存在),以便看到有#34;多个&# 34;分组中的文档,因此表示重叠。

这只是使用$unwind聚合管道阶段来解析/解密化&#34;数组内容,以便我们可以访问内部值进行分组。这正是在$group阶段发生的事情,其中​​&#34;键&#34;提供的是预订间隔ID,$push运营商用于&#34;收集&#34;有关在该组中找到的当前文档的数据。 $match如前所述。

这甚至可以扩展为替代演示:

db.booking.aggregate([
  { "$unwind": "$booking" },
  { "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
  { "$match": { "docs.1": { "$exists": true } } },
  { "$unwind": "$docs" },
  { "$group": {
    "_id": "$docs",
    "intervals": { "$push": "$_id" }  
  }}
])

输出:

{ "_id" : "B", "intervals" : [ 12 ] }
{ "_id" : "D", "intervals" : [ 10, 11 ] }
{ "_id" : "A", "intervals" : [ 10, 11, 12 ] }

这是一个简化的演示,但是如果您拥有的数据允许进行所需的分析,那么这是一种更有效的方法。所以如果你能保持&#34;粒度&#34;固定到&#34;设置&#34;可以通常记录在每个文档上的间隔,然后分析和报告可以使用后一种方法快速有效地识别这种重叠。

基本上,这就是你如何实现你基本上提到的更好的&#34;无论如何,第一个是&#34;轻微&#34;改进你最初理论化的东西。看看哪一个真的适合你的情况,但这应该解释实施和差异。