MongoDB .NET 驱动程序按时间范围分组

MongoDB .NET Driver Group By Time Range

我是 MongoDB 的菜鸟,想知道如何计算从世界标准时间凌晨 12 点开始到当前 UTC 时间每 15 分钟插入到集合中的文档总数。

下面是示例文档

{
    "_id" : ObjectId("5ade8bfc6b941c7726a54f01"),
    "Country" : "US"
    "Timestamp" : ISODate("2018-04-24T01:44:28.040Z"),
}

这是预期的输出:

{
    "Count": 245,
    "ReceiveDateString": "5/2/2018 12:00:00 AM"
},
{
    "Count": 239,
    "ReceiveDateString": "5/2/2018 12:15:00 AM"
},
{
    "Count": 252,
    "ReceiveDateString": "5/2/2018 12:30:00 AM"
},
{
    "Count": 255,
    "ReceiveDateString": "5/2/2018 12:45:00 AM"
},
{
    "Count": 242,
    "ReceiveDateString": "5/2/2018 1:00:00 AM"
}
.
.
.

and so on until current UTC time.

我可以按分钟分组如下:

var filter = Builders<Model>.Filter.Where(r => r.Timestamp > startDate && r.Timestamp < endDate);
var result = Collection.Aggregate()
           .Match(filter)
           .Group(
               r => r.Timestamp.Minute,
               g => new
               {
                   ReceiveDate = g.Select(x => x.Timestamp).First(),
                   Count = g.Count(),
               }
           ).ToEnumerable();

但是,我无法弄清楚如何将 Group result by 15 minutes time interval in MongoDb 中提供的解决方案转换为 MongoDB C# 驱动程序查询。

谢谢。

如果您正在寻找与 .NET 相关的 referenced post“确切的东西”,那么它实际上可能不会像那样实现。你可以这样做,但你可能不会费心去寻找其他选择之一,除非你像我一样需要“灵活的间隔”..

流利聚合

如果您有可用的现代 MongoDB 3.6 或更高版本的服务器,那么您可以使用 $dateFromParts 从日期中提取的“四舍五入”部分重建日期:

DateTime startDate = new DateTime(2018, 5, 1, 0, 0, 0, DateTimeKind.Utc);
DateTime endDate = new DateTime(2018, 6, 1, 0, 0, 0, DateTimeKind.Utc);

var result = Collection.Aggregate()
  .Match(k => k.Timestamp >= startDate && k.Timestamp < endDate)
  .Group(k =>
    new DateTime(k.Timestamp.Year, k.Timestamp.Month, k.Timestamp.Day,
        k.Timestamp.Hour, k.Timestamp.Minute - (k.Timestamp.Minute % 15), 0),
    g => new { _id = g.Key, count = g.Count() }
  )
  .SortBy(d => d._id)
  .ToList();

发送到服务器的语句:

[
  { "$match" : {
    "Timestamp" : {
      "$gte" : ISODate("2018-05-01T00:00:00Z"),
      "$lt" : ISODate("2018-06-01T00:00:00Z")
    }
  } },
  { "$group" : {
    "_id" : { 
      "$dateFromParts" : {
        "year" : { "$year" : "$Timestamp" },
        "month" : { "$month" : "$Timestamp" },
        "day" : { "$dayOfMonth" : "$Timestamp" },
        "hour" : { "$hour" : "$Timestamp" },
        "minute" : { "$subtract" : [
          { "$minute" : "$Timestamp" },
          { "$mod" : [ { "$minute" : "$Timestamp" }, 15 ] }
        ] },
        "second" : 0
      }
    },
    "count" : { "$sum" : 1 }
  } },
  { "$sort": { "_id": 1 } }
]

如果您没有该功能,那么您只需将其关闭并保留日期“disassembled”,然后在处理光标时再次assemble .只是用一个列表来模拟:

var result = Collection.Aggregate()
 .Match(k => k.Timestamp >= startDate && k.Timestamp < endDate)
 .Group(k => new
    {
      year = k.Timestamp.Year,
      month = k.Timestamp.Month,
      day = k.Timestamp.Day,
      hour = k.Timestamp.Hour,
      minute = k.Timestamp.Minute - (k.Timestamp.Minute % 15)
    },
    g => new { _id = g.Key, count = g.Count() }
  )
  .SortBy(d => d._id)
  .ToList();

foreach (var doc in result)
{
  //System.Console.WriteLine(doc.ToBsonDocument());
  System.Console.WriteLine(
    new BsonDocument {
      { "_id", new DateTime(doc._id.year, doc._id.month, doc._id.day,
        doc._id.hour, doc._id.minute, 0) },
      { "count", doc.count }
    }
  );
}

发送到服务器的语句:

[
  { "$match" : {
    "Timestamp" : {
      "$gte" : ISODate("2018-05-01T00:00:00Z"),
      "$lt" : ISODate("2018-06-01T00:00:00Z")
    }
  } },
  { "$group" : {
    "_id" : {
      "year" : { "$year" : "$Timestamp" },
      "month" : { "$month" : "$Timestamp" },
      "day" : { "$dayOfMonth" : "$Timestamp" },
      "hour" : { "$hour" : "$Timestamp" },
      "minute" : { "$subtract" : [
        { "$minute" : "$Timestamp" }, 
        { "$mod" : [ { "$minute" : "$Timestamp" }, 15 ] }
      ] }
    },
    "count" : { "$sum" : 1 }
  } },
  { "$sort" : { "_id" : 1 } }
]

两者在代码方面差别很小。只是在一种情况下,“转换回”DateTime 实际上发生在带有 $dateFromParts 的服务器上,而在另一种情况下,我们只是在代码中使用 DateTime 构造函数进行完全相同的转换当您迭代每个游标结果时。

所以它们实际上几乎相同,唯一真正的区别是“服务器”执行日期转换的位置 returned 每个文档使用的字节数要少得多。实际上少了“5 倍”,因为这里的所有数字格式(包括 BSON 日期)都基于 64 位整数。即便如此,所有这些数字实际上仍然比发回日期的任何“字符串”表示形式“更轻”。

LINQ 可查询

这些基本形式在映射到这些不同形式时真正保持不变:

var query = from p in Collection.AsQueryable()
            where p.Timestamp >= startDate && p.Timestamp < endDate
            group p by new DateTime(p.Timestamp.Year, p.Timestamp.Month, p.Timestamp.Day,
              p.Timestamp.Hour, p.Timestamp.Minute - (p.Timestamp.Minute % 15), 0) into g
            orderby g.Key
            select new { _id = g.Key, count = g.Count() };

发送到服务器的语句:

[
  { "$match" : {
    "Timestamp" : {
      "$gte" : ISODate("2018-05-01T00:00:00Z"),
      "$lt" : ISODate("2018-06-01T00:00:00Z")
    }
  } },
  { "$group" : {
    "_id" : {
      "$dateFromParts" : {
        "year" : { "$year" : "$Timestamp" }, 
        "month" : { "$month" : "$Timestamp" },
        "day" : { "$dayOfMonth" : "$Timestamp" }, 
        "hour" : { "$hour" : "$Timestamp" }, 
        "minute" : { "$subtract" : [
          { "$minute" : "$Timestamp" },
          { "$mod" : [ { "$minute" : "$Timestamp" }, 15 ] }
        ] },
        "second" : 0
      }
    },
    "__agg0" : { "$sum" : 1 }
  } },
  { "$sort" : { "_id" : 1 } },
  { "$project" : { "_id" : "$_id", "count" : "$__agg0" } }
]

或使用GroupBy()

var query = Collection.AsQueryable()
    .Where(k => k.Timestamp >= startDate && k.Timestamp < endDate)
    .GroupBy(k =>
      new DateTime(k.Timestamp.Year, k.Timestamp.Month, k.Timestamp.Day,
            k.Timestamp.Hour, k.Timestamp.Minute - (k.Timestamp.Minute % 15), 0),
      (k, s) => new { _id = k, count = s.Count() }
    )
    .OrderBy(k => k._id);

发送到服务器的语句:

[
  { "$match" : {
    "Timestamp" : {
      "$gte" : ISODate("2018-05-01T00:00:00Z"),
      "$lt" : ISODate("2018-06-01T00:00:00Z")
    }
  } },
  { "$group" : {
    "_id" : {
      "$dateFromParts" : {
        "year" : { "$year" : "$Timestamp" },
        "month" : { "$month" : "$Timestamp" },
        "day" : { "$dayOfMonth" : "$Timestamp" },
        "hour" : { "$hour" : "$Timestamp" },
        "minute" : { "$subtract" : [ 
          { "$minute" : "$Timestamp" }, 
          { "$mod" : [ { "$minute" : "$Timestamp" }, 15 ] } 
        ] },
        "second" : 0
      }
    },
    "count" : { "$sum" : 1 }
  } },
  { "$sort" : { "_id" : 1 } }
]

如您所见,它们基本上都是相同的形式


转换原始文件

如果您希望复制 the original "date math" form as posted,那么它目前超出了您使用 LINQ 或 Fluent 构建器实际执行的范围。获得相同序列的唯一方法是使用 BsonDocument 构造:

DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

var group = new BsonDocument { {
  "$group",
  new BsonDocument {
    { "_id",
    new BsonDocument { {
      "$add", new BsonArray
      {
        new BsonDocument { {
            "$subtract",
            new BsonArray {
              new BsonDocument { { "$subtract", new BsonArray { "$Timestamp", epoch } } },
              new BsonDocument { {
                "$mod", new BsonArray
                {
                 new BsonDocument { { "$subtract", new BsonArray { "$Timestamp", epoch } } },
                 1000 * 60 * 15
               }
             } }
           }
         } },
         epoch
       }
     } }
     },
     {
       "count", new BsonDocument("$sum", 1)
     }
   }
} };

var query = sales.Aggregate()
  .Match(k => k.Timestamp >= startDate && k.Timestamp < endDate)
  .AppendStage<BsonDocument>(group)
  .Sort(new BsonDocument("_id", 1))
  .ToList();

发送到服务器的请求:

[
  { "$match" : {
    "Timestamp" : {
      "$gte" : ISODate("2018-05-01T00:00:00Z"),
      "$lt" : ISODate("2018-06-01T00:00:00Z")
    }
  } },
  { "$group" : {
    "_id" : { 
      "$add" : [
        { "$subtract" : [ 
          { "$subtract" : [ "$Timestamp", ISODate("1970-01-01T00:00:00Z") ] },
          { "$mod" : [ 
            { "$subtract" : [ "$Timestamp", ISODate("1970-01-01T00:00:00Z") ] },
            900000
          ] }
        ] },
        ISODate("1970-01-01T00:00:00Z")
      ]
    },
    "count" : { "$sum" : 1 }
  } },
  { "$sort" : { "_id" : 1 } }
]

我们现在不能这样做的一个重要原因是因为当前的语句序列化基本上不同意 .NET Framework 所说的减去两个 DateTime 值 return a TimeSpan,以及减去两个 BSON 日期的 MongoDB 构造 return 是“自纪元以来的毫秒数”,这基本上就是数学的工作原理。

lamdba 表达式的“字面”翻译本质上是:

p =>  epoch.AddMilliseconds(
       (p.Timestamp - epoch).TotalMilliseconds
       - ((p.Timestamp - epoch).TotalMilliseconds % 1000 * 60 * 15))

但是映射仍然需要做一些工作才能识别语句或形式化哪种语句实际用于此目的。

值得注意的是 MongoDB 4.0 引入了 $convert operator and the common aliases of $toLong and $toDate,它们都可以在管道中使用,代替当前对 BSON 日期的“加法”和“减法”处理。这些开始为此类转换形成更“正式”的规范,而不是所示的仅依赖于“加法”和“减法”的方法,这仍然有效,但此类命名运算符在代码中的意图更加清晰:

{ "$group": {
  "_id": {
    "$toDate": {
      "$subtract": [
        { "$toLong": "$Timestamp" },
        { "$mod": [{ "$toLong": "$Timestamp" }, 1000 * 60 * 15 ] }
      ]
    }
  },
  "count": { "$sum": 1 }
}}

很明显,对于此类“DateToLong”和“LongToDate”函数,使用 LINQ 语句构造的“形式化”运算符,如果没有“非工作”正在完成 lambda 表达式。