MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Groupe de pilotes MongoDB .NET par plage de temps

Si vous cherchez la "chose exacte" comme le post référencé à faire avec .NET, alors il ne sera probablement pas implémenté comme ça. Vous pouvez le faire, mais vous n'allez probablement pas vous donner la peine de choisir l'une des autres alternatives, à moins que vous n'ayez besoin d'"intervalles flexibles" dans la mesure où je le fais.

Agrégat fluide

Si vous disposez d'un serveur MongoDB 3.6 ou supérieur moderne, vous pouvez utiliser $dateFromParts afin de reconstituer la date à partir des parties "arrondies" extraites de la date :

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

Relevé envoyé au serveur :

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

Si cette fonctionnalité n'est pas disponible, vous pouvez simplement la désactiver et laisser la date "désassemblée", puis la réassembler au fur et à mesure que vous traitez le curseur. Juste pour simuler avec une liste :

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

Relevé envoyé au serveur :

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

Il y a très peu de différence entre les deux en termes de code. C'est juste que dans un cas, le "rejeu" à DateTime se produit réellement sur le serveur avec le $dateFromParts et dans l'autre, nous faisons exactement le même casting en utilisant le DateTime constructeur dans le code lorsque vous itérez chaque résultat de curseur.

Donc, ils sont vraiment presque les mêmes, la seule vraie différence étant l'endroit où le "serveur" fait le casting de la date renvoyée utilise beaucoup moins d'octets par document. En fait "5 fois" moins puisque tous les formats numériques ici (y compris la date BSON) sont basés sur des entiers 64 bits. Même ainsi, tous ces nombres sont toujours "plus légers" que de renvoyer une représentation "chaîne" d'une date.

Requête LINQ

Ce sont les formulaires de base qui restent vraiment les mêmes lors de la cartographie sur ces différents formulaires :

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

Relevé envoyé au serveur :

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

Ou en utilisant 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);

Relevé envoyé au serveur :

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

Comme vous pouvez le voir, c'est essentiellement le même formulaire

Convertir l'original

Si vous cherchez à reproduire le formulaire "date math" d'origine tel que publié, cela dépasse actuellement le cadre de ce que vous pouvez réellement faire avec LINQ ou les constructeurs Fluent. La seule façon d'obtenir cette même séquence est avec BsonDocument fabrication :

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

Requête envoyée au serveur :

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

La grande raison pour laquelle nous ne pouvons pas le faire pour le moment est que la sérialisation actuelle des instructions n'est pas d'accord sur le fait que le .NET Framework dit que la soustraction de deux DateTime les valeurs renvoient un TimeSpan , et la construction MongoDB de soustraction de deux dates BSON renvoie les "millisecondes depuis l'époque", ce qui est essentiellement la façon dont les mathématiques fonctionnent.

La traduction "littérale" de l'expression lamdba est essentiellement :

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

Mais la cartographie a encore besoin d'un peu de travail afin de reconnaître les déclarations ou de formaliser sur quel type de déclarations sont réellement destinées à cet effet.

Notamment MongoDB 4.0 introduit le $convert opérateur et les alias communs de $toLong et $toDate , qui peuvent tous être utilisés dans le pipeline à la place de la gestion actuelle de "l'addition" et de la "soustraction" avec les dates BSON. Celles-ci commencent à former une spécification plus "formelle" pour de telles conversions plutôt que la méthode illustrée qui reposait uniquement sur cette "addition" et cette "soustraction", qui est toujours valable, mais ces opérateurs nommés sont beaucoup plus clairs dans le code :

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

Il est assez simple de voir qu'avec des opérateurs "formalisés" pour la construction d'instructions avec LINQ pour de telles fonctions "DateToLong" et "LongToDate", alors l'instruction devient beaucoup plus propre sans les types de "coercitions" montrées dans l'expression lambda "non fonctionnelle" étant terminé.