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

MongoDB-Aggregation – $group by date, auch wenn nicht vorhanden

Anstatt zu versuchen, die Datenbank dazu zu zwingen, Ergebnisse für nicht vorhandene Daten zurückzugeben, ist es besser, die leeren Daten außerhalb der Abfrage zu generieren und die Ergebnisse darin zusammenzuführen. Auf diese Weise haben Sie Ihre "0"-Einträge, wo keine Daten vorhanden sind, und ermöglichen der Datenbank, das zurückzugeben, was vorhanden ist.

Das Zusammenführen ist ein grundlegender Prozess zum Erstellen einer Hash-Tabelle mit eindeutigen Schlüsseln und zum einfachen Ersetzen aller Werte, die in den Aggregationsergebnissen in dieser Hash-Tabelle gefunden werden. In JavaScript passt ein einfaches Objekt gut, da alle Schlüssel eindeutig sind.

Ich ziehe es auch vor, tatsächlich ein Date zurückzugeben Objekt aus Aggregationsergebnissen, indem Sie Datumsmathematik verwenden, um das Datum zu manipulieren und auf das erforderliche Intervall zu "runden", anstatt die Datumsaggregationsoperatoren zu verwenden. Sie können Daten mit $subtract manipulieren um den Wert in eine numerische Zeitstempeldarstellung umzuwandeln, indem von einem anderen Datum mit dem Epochendatumswert subtrahiert wird, und der $mod -Operator, um den Rest zu erhalten und das Datum auf das erforderliche Intervall zu runden.

Im Gegensatz dazu wird $add verwendet mit einem ähnlichen Epochendatumsobjekt wird einen ganzzahligen Wert wieder in ein BSON-Datum umwandeln. Und natürlich ist es viel effizienter, direkt zum $group anstatt ein separates $project zu verwenden da Sie die geänderten Daten einfach direkt in die Gruppierung _id verarbeiten können Wert sowieso.

Als Shell-Beispiel:

var sample = 30,
    Days = 30,
    OneDay = ( 1000 * 60 * 60 * 24 ),
    now = Date.now(),
    Today = now - ( now % OneDay ) ,
    nDaysAgo = Today - ( OneDay * Days ),
    startDate = new Date( nDaysAgo ),
    endDate = new Date( Today + OneDay ),
    store = {};

var thisDay = new Date( nDaysAgo );
while ( thisDay < endDate ) {
    store[thisDay] = 0;
    thisDay = new Date( thisDay.valueOf() + OneDay );
}

db.datejunk.aggregate([
    { "$match": { "when": { "$gte": startDate } }},
    { "$group": {
        "_id": {
            "$add": [
                { "$subtract": [
                    { "$subtract": [ "$when", new Date(0) ] },
                    { "$mod": [
                        { "$subtract": [ "$when", new Date(0) ] },
                        OneDay
                    ]}
                ]},
                new Date(0)
            ]
        },
        "count": { "$sum": 1 }
    }}
]).forEach(function(result){
    store[result._id] = result.count;
});

Object.keys(store).forEach(function(k) {
    printjson({ "date": k, "count": store[k] })
});

Dadurch werden alle Tage im Intervall zurückgegeben, einschließlich 0 Werte, wo keine Daten vorhanden sind, wie:

{ "date" : "Tue Sep 22 2015 10:00:00 GMT+1000 (AEST)", "count" : 0 }
{ "date" : "Wed Sep 23 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Thu Sep 24 2015 10:00:00 GMT+1000 (AEST)", "count" : 0 }
{ "date" : "Fri Sep 25 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Sat Sep 26 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Sun Sep 27 2015 10:00:00 GMT+1000 (AEST)", "count" : 0 }
{ "date" : "Mon Sep 28 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Tue Sep 29 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Wed Sep 30 2015 10:00:00 GMT+1000 (AEST)", "count" : 0 }
{ "date" : "Thu Oct 01 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Fri Oct 02 2015 10:00:00 GMT+1000 (AEST)", "count" : 2 }
{ "date" : "Sat Oct 03 2015 10:00:00 GMT+1000 (AEST)", "count" : 0 }
{ "date" : "Sun Oct 04 2015 11:00:00 GMT+1100 (AEST)", "count" : 1 }
{ "date" : "Mon Oct 05 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Tue Oct 06 2015 11:00:00 GMT+1100 (AEDT)", "count" : 1 }
{ "date" : "Wed Oct 07 2015 11:00:00 GMT+1100 (AEDT)", "count" : 2 }
{ "date" : "Thu Oct 08 2015 11:00:00 GMT+1100 (AEDT)", "count" : 2 }
{ "date" : "Fri Oct 09 2015 11:00:00 GMT+1100 (AEDT)", "count" : 1 }
{ "date" : "Sat Oct 10 2015 11:00:00 GMT+1100 (AEDT)", "count" : 1 }
{ "date" : "Sun Oct 11 2015 11:00:00 GMT+1100 (AEDT)", "count" : 1 }
{ "date" : "Mon Oct 12 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Tue Oct 13 2015 11:00:00 GMT+1100 (AEDT)", "count" : 3 }
{ "date" : "Wed Oct 14 2015 11:00:00 GMT+1100 (AEDT)", "count" : 2 }
{ "date" : "Thu Oct 15 2015 11:00:00 GMT+1100 (AEDT)", "count" : 2 }
{ "date" : "Fri Oct 16 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Sat Oct 17 2015 11:00:00 GMT+1100 (AEDT)", "count" : 3 }
{ "date" : "Sun Oct 18 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Mon Oct 19 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Tue Oct 20 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Wed Oct 21 2015 11:00:00 GMT+1100 (AEDT)", "count" : 2 }
{ "date" : "Thu Oct 22 2015 11:00:00 GMT+1100 (AEDT)", "count" : 1 }

Beachten Sie, dass alle „date“-Werte eigentlich immer noch BSON-Daten sind, aber einfach so in der Ausgabe von .printjson() stringifizieren als Shell-Methode.

Ein etwas prägnanteres Beispiel kann mit nodejs gezeigt werden wo Sie Operationen wie async.parallel verwenden können um sowohl die Hash-Konstruktion als auch die Aggregationsabfrage gleichzeitig zu verarbeiten, sowie ein weiteres nützliches Dienstprogramm in nedb die den "Hash" mit Funktionen implementiert, die mit der Verwendung einer MongoDB-Sammlung vertraut sind. Es zeigt auch, wie dies für große Ergebnisse skaliert werden kann, indem eine echte MongoDB-Sammlung verwendet wird, wenn Sie auch die Verarbeitung auf Stream-Verarbeitung des zurückgegebenen Cursors von .aggregate() geändert haben :

var async = require('async'),
    mongodb = require('mongodb'),
    MongoClient = mongodb.MongoClient,
    nedb = require('nedb'),
    DataStore = new nedb();

// Setup vars
var sample = 30,
    Days = 30,
    OneDay = ( 1000 * 60 * 60 * 24 ),
    now = Date.now(),
    Today = now - ( now % OneDay ) ,
    nDaysAgo = Today - ( OneDay * Days ),
    startDate = new Date( nDaysAgo ),
    endDate = new Date( Today + OneDay );

MongoClient.connect('mongodb://localhost/test',function(err,db) {

  var coll = db.collection('datejunk');

  async.series(
    [
      // Clear test collection
      function(callback) {
        coll.remove({},callback)
      },

      // Generate a random sample
      function(callback) {
        var bulk = coll.initializeUnorderedBulkOp();

        while (sample--) {
          bulk.insert({
            "when": new Date(
              Math.floor(
                Math.random()*(Today-nDaysAgo+OneDay)+nDaysAgo
              )
            )
          });
        }
        bulk.execute(callback);
      },

      // Aggregate data and dummy data
      function(callback) {
        console.log("generated");
        async.parallel(
          [
            // Dummy data per day
            function(callback) {
              var thisDay = new Date( nDaysAgo );
              async.whilst(
                function() { return thisDay < endDate },
                function(callback) {
                  DataStore.update(
                    { "date": thisDay },
                    { "$inc": { "count": 0 } },
                    { "upsert": true },
                    function(err) {
                      thisDay = new Date( thisDay.valueOf() + OneDay );
                      callback(err);
                    }
                  );
                },
                callback
              );
            },
            // Aggregate data in collection
            function(callback) {
              coll.aggregate(
                [
                  { "$match": { "when": { "$gte": startDate } } },
                  { "$group": {
                    "_id": {
                      "$add": [
                        { "$subtract": [
                          { "$subtract": [ "$when", new Date(0) ] },
                          { "$mod": [
                            { "$subtract": [ "$when", new Date(0) ] },
                            OneDay
                          ]}
                        ]},
                        new Date(0)
                      ]
                    },
                    "count": { "$sum": 1 }
                  }}
                ],
                function(err,results) {
                  if (err) callback(err);
                  async.each(results,function(result,callback) {
                    DataStore.update(
                      { "date": result._id },
                      { "$inc": { "count": result.count } },
                      { "upsert": true },
                      callback
                    );
                  },callback);
                }
              );
            }
          ],
          callback
        );
      }
    ],
    // Return result or error
    function(err) {
      if (err) throw err;
      DataStore.find({},{ "_id": 0 })
        .sort({ "date": 1 })
        .exec(function(err,results) {
        if (err) throw err;
        console.log(results);
        db.close();
      });
    }
  );

});

Dies eignet sich sehr gut für Daten für Diagramme und Grafiken. Das grundlegende Verfahren ist für jede Sprachimplementierung gleich und wird für die beste Leistung idealerweise in paralleler Verarbeitung durchgeführt. Asynchrone oder Thread-Umgebungen bieten Ihnen also einen echten Bonus, auch wenn für ein kleines Beispiel wie dieses die grundlegende Hash-Tabelle sehr schnell im Speicher generiert werden kann Ihrer Umgebung erfordert sequentielle Operationen.

Versuchen Sie also nicht, die Datenbank dazu zu zwingen. Es gibt sicherlich Beispiele für SQL-Abfragen, die diese "Zusammenführung" auf dem Datenbankserver durchführen, aber es war dort nie wirklich eine gute Idee und sollte wirklich mit einem ähnlichen "Client" -Mergeprozess behandelt werden, da es nur Datenbank-Overhead erzeugt, was wirklich nicht der Fall ist. Nicht erforderlich.

Das alles ist sehr effizient und praktisch für den Zweck und erfordert natürlich nicht die Verarbeitung einer separaten Aggregationsabfrage für jeden Tag im Zeitraum, was überhaupt nicht effizient wäre.