MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

MongoDB .NET-Treibergruppe nach Zeitbereich

Wenn Sie nach dem "genauen Ding" suchen, auf das der Beitrag mit .NET verweist, wird es wahrscheinlich nicht so implementiert. Sie können das tun, aber Sie werden sich wahrscheinlich nicht die ganze Mühe machen und tatsächlich eine der anderen Alternativen wählen, es sei denn, Sie brauchen "flexible Intervalle" in dem Maße, wie ich es tue.

Fließendes Aggregat

Wenn Sie über einen modernen MongoDB-Server der Version 3.6 oder höher verfügen, können Sie $dateFromParts verwenden um das Datum aus den aus dem Datum extrahierten "gerundeten" Teilen zu rekonstruieren:

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();

An den Server gesendete Anweisung:

[
  { "$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 } }
]

Wenn Sie diese Funktion nicht zur Verfügung haben, können Sie sie einfach weglassen und das Datum "zerlegt" belassen, es dann aber wieder zusammenbauen, während Sie den Cursor bearbeiten. Nur um mit einer Liste zu simulieren:

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

An den Server gesendete Anweisung:

[
  { "$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 } }
]

Es gibt sehr wenig Unterschied zwischen den beiden in Bezug auf den Code. Es ist nur in einem Fall das "Zurückwerfen" auf DateTime passiert tatsächlich auf dem Server mit den $dateFromParts und in der anderen machen wir genau das gleiche Casting mit DateTime Konstruktor im Code, während Sie jedes Cursor-Ergebnis durchlaufen.

Sie sind also wirklich fast gleich, mit dem einzigen wirklichen Unterschied, wo der "Server" das Casting des zurückgegebenen Datums durchführt und viel weniger Bytes pro Dokument verwendet. Tatsächlich "5-mal" weniger, da alle numerischen Formate hier (einschließlich des BSON-Datums) auf 64-Bit-Ganzzahlen basieren. Trotzdem sind all diese Zahlen immer noch "leichter" als das Zurücksenden einer "String"-Darstellung eines Datums.

LINQ-Abfrage

Das sind die Grundformen, die beim Mapping auf diese verschiedenen Formen wirklich gleich bleiben:

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

An den Server gesendete Anweisung:

[
  { "$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" } }
]

Oder mit 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);

An den Server gesendete Anweisung:

[
  { "$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 } }
]

Wie Sie sehen können, ist es im Grunde alles das gleiche Formular

Konvertieren des Originals

Wenn Sie das ursprüngliche „Datumsmathematik“-Formular wie veröffentlicht replizieren möchten, liegt dies derzeit außerhalb des Rahmens dessen, was Sie tatsächlich mit LINQ oder den Fluent-Buildern tun können. Dieselbe Sequenz erhalten Sie nur mit BsonDocument Aufbau:

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();

Anfrage an Server gesendet:

[
  { "$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 } }
]

Der Hauptgrund, warum wir dies derzeit nicht tun können, liegt darin, dass die aktuelle Serialisierung der Anweisungen grundsätzlich nicht mit dem Punkt übereinstimmt, den das .NET Framework besagt, dass das Subtrahieren von zwei DateTime Werte geben eine TimeSpan zurück , und das MongoDB-Konstrukt zum Subtrahieren von zwei BSON-Daten gibt die „Millisekunden seit Epoche“ zurück, was im Wesentlichen der Funktionsweise der Mathematik entspricht.

Die "wörtliche" Übersetzung des Lamba-Ausdrucks lautet im Wesentlichen:

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

Aber das Mapping erfordert noch etwas Arbeit, um entweder die Aussagen zu erkennen oder zu formalisieren, welche Art von Aussagen eigentlich für diesen Zweck gedacht sind.

Insbesondere MongoDB 4.0 führt den $convert ein -Operator und die allgemeinen Aliase von $toLong und $toDate , die alle in der Pipeline anstelle der aktuellen Behandlung von "Addition" und "Subtraktion" mit BSON-Daten verwendet werden können. Diese bilden eher eine "formellere" Spezifikation für solche Konvertierungen als die gezeigte Methode, die sich ausschließlich auf diese "Addition" und "Subtraktion" stützte, die immer noch gültig ist, aber solche benannten Operatoren sind innerhalb des Codes viel klarer von der Absicht:

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

Es ist ziemlich einfach zu sehen, dass mit „formalisierten“ Operatoren für die Anweisungskonstruktion mit LINQ für solche „DateToLong“- und „LongToDate“-Funktionen die Anweisung dann viel sauberer wird, ohne dass die Arten von „Zwängen“, die im „nicht funktionierenden“ Lambda-Ausdruck gezeigt werden, vorhanden sind erledigt.