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

Mongodb aggregierte Sortierung und Begrenzung innerhalb der Gruppe

Das Grundproblem

Es ist nicht die klügste Idee, dies derzeit und in absehbarer naher Zukunft im Aggregations-Framework zu versuchen. Das Hauptproblem kommt natürlich von dieser Zeile in dem Code, den Sie bereits haben:

"items" : { "$push": "$$ROOT" }

Und das bedeutet genau das, was im Grunde passieren muss, ist, dass alle Objekte innerhalb des Gruppierungsschlüssels in ein Array geschoben werden müssen, um in jedem späteren Code zu den "top N"-Ergebnissen zu gelangen.

Dies skaliert eindeutig nicht, da die Größe dieses Arrays selbst möglicherweise die BSON-Grenze von 16 MB überschreiten kann, und zwar unabhängig von den restlichen Daten im gruppierten Dokument. Der größte Haken dabei ist, dass es nicht möglich ist, den Push nur auf eine bestimmte Anzahl von Artikeln zu beschränken. Es gibt ein langjähriges JIRA-Problem zu genau so etwas.

Allein aus diesem Grund besteht der praktischste Ansatz darin, einzelne Abfragen für die "Top-N"-Elemente für jeden Gruppierungsschlüssel auszuführen. Diese müssen nicht einmal .aggregate() sein Aussagen (abhängig von den Daten) und kann wirklich alles sein, was einfach die gewünschten "Top N"-Werte einschränkt.

Bester Ansatz

Ihre Architektur scheint sich auf node.js zu befinden mit mongoose , aber alles, was asynchrone E / A und die parallele Ausführung von Abfragen unterstützt, ist die beste Option. Idealerweise etwas mit einer eigenen API-Bibliothek, die das Kombinieren der Ergebnisse dieser Abfragen in einer einzigen Antwort unterstützt.

Zum Beispiel gibt es diese vereinfachte Beispielliste, die Ihre Architektur und verfügbaren Bibliotheken verwendet (insbesondere async ), die dies parallel tut und Ergebnisse genau kombiniert:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var data = [
  { "merchant": 1, "rating": 1 },
  { "merchant": 1, "rating": 2 },
  { "merchant": 1, "rating": 3 },
  { "merchant": 2, "rating": 1 },
  { "merchant": 2, "rating": 2 },
  { "merchant": 2, "rating": 3 }
];

var testSchema = new Schema({
  merchant: Number,
  rating: Number
});

var Test = mongoose.model( 'Test', testSchema, 'test' );

async.series(
  [
    function(callback) {
      Test.remove({},callback);
    },
    function(callback) {
      async.each(data,function(item,callback) {
        Test.create(item,callback);
      },callback);
    },
    function(callback) {
      async.waterfall(
        [
          function(callback) {
            Test.distinct("merchant",callback);
          },
          function(merchants,callback) {
            async.concat(
              merchants,
              function(merchant,callback) {
                Test.find({ "merchant": merchant })
                  .sort({ "rating": -1 })
                  .limit(2)
                  .exec(callback);
              },
              function(err,results) {
                console.log(JSON.stringify(results,undefined,2));
                callback(err);
              }
            );
          }
        ],
        callback
      );
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

Das Ergebnis sind nur die beiden besten Ergebnisse für jeden Händler in der Ausgabe:

[
  {
    "_id": "560d153669fab495071553ce",
    "merchant": 1,
    "rating": 3,
    "__v": 0
  },
  {
    "_id": "560d153669fab495071553cd",
    "merchant": 1,
    "rating": 2,
    "__v": 0
  },
  {
    "_id": "560d153669fab495071553d1",
    "merchant": 2,
    "rating": 3,
    "__v": 0
  },
  {
    "_id": "560d153669fab495071553d0",
    "merchant": 2,
    "rating": 2,
    "__v": 0
  }
]

Dies ist wirklich der effizienteste Weg, dies zu verarbeiten, obwohl es Ressourcen in Anspruch nehmen wird, da es sich immer noch um mehrere Abfragen handelt. Aber bei weitem nicht die Ressourcen, die in der Aggregationspipeline verbraucht werden, wenn Sie versuchen, alle Dokumente in einem Array zu speichern und zu verarbeiten.

Das Gesamtproblem, jetzt und in naher Zukunft

In Anbetracht dessen, dass die Anzahl der Dokumente keine Verletzung der BSON-Grenze verursacht, ist dies möglich. Methoden mit der aktuellen Version von MongoDB sind dafür nicht besonders geeignet, aber die kommende Version (zum Zeitpunkt des Schreibens tut dies der Dev-Zweig 3.1.8) führt zumindest ein $slice ein Betreiber an die Aggregationspipeline. Wenn Sie also bei der Aggregationsoperation klüger sind, verwenden Sie einen $sort zuerst, dann können die bereits sortierten Elemente im Array einfach herausgesucht werden:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var data = [
  { "merchant": 1, "rating": 1 },
  { "merchant": 1, "rating": 2 },
  { "merchant": 1, "rating": 3 },
  { "merchant": 2, "rating": 1 },
  { "merchant": 2, "rating": 2 },
  { "merchant": 2, "rating": 3 }
];

var testSchema = new Schema({
  merchant: Number,
  rating: Number
});

var Test = mongoose.model( 'Test', testSchema, 'test' );

async.series(
  [
    function(callback) {
      Test.remove({},callback);
    },
    function(callback) {
      async.each(data,function(item,callback) {
        Test.create(item,callback);
      },callback);
    },
    function(callback) {
      Test.aggregate(
        [
          { "$sort": { "merchant": 1, "rating": -1 } },
          { "$group": {
            "_id": "$merchant",
            "items": { "$push": "$$ROOT" }
          }},
          { "$project": {
            "items": { "$slice": [ "$items", 2 ] }
          }}
        ],
        function(err,results) {
          console.log(JSON.stringify(results,undefined,2));
          callback(err);
        }
      );
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

Was das gleiche grundlegende Ergebnis liefert, da die obersten 2 Elemente aus dem Array "geschnitten" werden, sobald sie zuerst sortiert wurden.

Es ist auch in aktuellen Versionen "möglich", aber mit den gleichen grundlegenden Einschränkungen, da dies immer noch das Verschieben aller Inhalte in ein Array beinhaltet, nachdem die Inhalte zuerst sortiert wurden. Es braucht nur einen "iterativen" Ansatz. Sie können dies auscodieren, um die Aggregationspipeline für größere Einträge zu erstellen, aber nur „zwei“ anzuzeigen, sollte zeigen, dass es keine wirklich gute Idee ist, es zu versuchen:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var data = [
  { "merchant": 1, "rating": 1 },
  { "merchant": 1, "rating": 2 },
  { "merchant": 1, "rating": 3 },
  { "merchant": 2, "rating": 1 },
  { "merchant": 2, "rating": 2 },
  { "merchant": 2, "rating": 3 }
];

var testSchema = new Schema({
  merchant: Number,
  rating: Number
});

var Test = mongoose.model( 'Test', testSchema, 'test' );

async.series(
  [
    function(callback) {
      Test.remove({},callback);
    },
    function(callback) {
      async.each(data,function(item,callback) {
        Test.create(item,callback);
      },callback);
    },
    function(callback) {
      Test.aggregate(
        [
          { "$sort": { "merchant": 1, "rating": -1 } },
          { "$group": {
            "_id": "$merchant",
            "items": { "$push": "$$ROOT" }
          }},
          { "$unwind": "$items" },
          { "$group": {
            "_id": "$_id",
            "first": { "$first": "$items" },
            "items": { "$push": "$items" }
          }},
          { "$unwind": "$items" },
          { "$redact": {
            "$cond": [
              { "$eq": [ "$items", "$first" ] },
              "$$PRUNE",
              "$$KEEP"
            ]
          }},
          { "$group": {
            "_id": "$_id",
            "first": { "$first": "$first" },
            "second": { "$first": "$items" }
          }},
          { "$project": {
            "items": {
              "$map": {
                "input": ["A","B"],
                "as": "el",
                "in": {
                  "$cond": [
                    { "$eq": [ "$$el", "A" ] },
                    "$first",
                    "$second"
                  ]
                }
              }
            }
          }}
        ],
        function(err,results) {
          console.log(JSON.stringify(results,undefined,2));
          callback(err);
        }
      );
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

Und wieder, obwohl es in früheren Versionen "möglich" war (dies verwendet die in 2.6 eingeführten Funktionen zum Kürzen, da Sie bereits $$ROOT taggen ), bestehen die grundlegenden Schritte darin, das Array zu speichern und dann jedes Element mit $first "vom Stapel" zu holen und dieses (und möglicherweise andere) mit Elementen innerhalb des Arrays zu vergleichen, um sie zu entfernen und dann das "nächste erste" Element von diesem Stapel zu holen, bis Ihr "top N" schließlich fertig ist.

Schlussfolgerung

Bis der Tag kommt, an dem es eine solche Operation gibt, die die Elemente in einem $push zulässt Aggregationsakkumulator auf eine bestimmte Anzahl begrenzt werden soll, dann ist dies keine wirklich praktische Operation für Aggregation.

Sie können dies tun, wenn die Daten, die Sie in diesen Ergebnissen haben, klein genug sind, und es könnte sogar effizienter sein als die Verarbeitung auf der Clientseite, wenn die Datenbankserver über ausreichende Spezifikationen verfügen, um einen echten Vorteil zu bieten. Aber die Chancen stehen gut, dass beides in den meisten realen Anwendungen mit angemessener Nutzung nicht der Fall sein wird.

Am besten verwenden Sie die zuerst gezeigte Option "Parallele Abfrage". Es lässt sich immer gut skalieren, und es besteht keine Notwendigkeit, eine solche Logik "umzucodieren", dass eine bestimmte Gruppierung möglicherweise nicht mindestens die gesamten erforderlichen "Top-N"-Elemente zurückgibt, und herauszufinden, wie sie beibehalten werden können (ein viel längeres Beispiel dafür wurde weggelassen ), da es einfach jede Abfrage durchführt und die Ergebnisse kombiniert.

Verwenden Sie parallele Abfragen. Es wird besser sein als der codierte Ansatz, den Sie haben, und es wird den demonstrierten Aggregationsansatz bei weitem übertreffen. Zumindest bis es eine bessere Option gibt.