sql >> Databasteknik >  >> NoSQL >> MongoDB

Mongodb samlar sortering och gräns inom gruppen

Det grundläggande problemet

Det är inte den klokaste idén där ute att försöka göra detta i den nuvarande aggregeringsramen inom en överskådlig nära framtid. Huvudproblemet kommer naturligtvis från denna rad i koden du redan har:

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

Och det betyder precis det, i och med att det som i princip måste hända är att alla objekt inom grupperingsnyckeln måste tryckas in i en array för att komma till "top N"-resultaten i någon senare kod.

Detta skalas uppenbarligen inte eftersom storleken på själva arrayen så småningom mycket tänkbart kan överskrida BSON-gränsen på 16 MB, och oavsett resten av data i det grupperade dokumentet. Den huvudsakliga haken här är att det inte är möjligt att "begränsa push" till bara ett visst antal objekt. Det finns en långvarig JIRA-fråga om just en sådan sak.

Bara av den anledningen är det mest praktiska tillvägagångssättet att köra individuella frågor för de "översta N" objekten för varje grupperingsnyckel. Dessa behöver inte ens vara .aggregate() uttalanden (beroende på data) och kan verkligen vara vad som helst som helt enkelt begränsar de "top N"-värden du vill ha.

Bästa tillvägagångssätt

Din arkitektur verkar vara på node.js med mongoose , men allt som stöder asynkron IO och parallell exekvering av frågor kommer att vara det bästa alternativet. Helst något med sitt eget API-bibliotek som stöder att kombinera resultaten av dessa frågor till ett enda svar.

Till exempel finns den här förenklade exemplet med din arkitektur och tillgängliga bibliotek (särskilt async ) som gör detta parallella och kombinerade resultat exakt:

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

Detta resulterar i bara de två bästa resultaten för varje handlare i utgången:

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

Det är verkligen det mest effektiva sättet att bearbeta detta även om det kommer att ta resurser eftersom det fortfarande är flera frågor. Men inte i närheten av de resurser som äts upp i aggregeringspipelinen om du försöker lagra alla dokument i en array och bearbeta den.

Det samlade problemet, nu och nära framtid

Till den raden är det möjligt med tanke på att antalet dokument inte orsakar ett brott i BSON-gränsen att detta kan göras. Metoder med den nuvarande versionen av MongoDB är inte bra för detta, men den kommande utgåvan (i skrivande stund gör 3.1.8 dev-grenen detta) introducerar åtminstone en $slice operatör till aggregeringsrörledningen. Så om du är smartare när det gäller aggregeringsoperationen och använder en $sort först, sedan kan de redan sorterade objekten i arrayen enkelt plockas ut:

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

Vilket ger samma grundläggande resultat som de två översta objekten "skivas" från arrayen när de sorterades först.

Det är faktiskt också "möjligt" i nuvarande utgåvor, men med samma grundläggande begränsningar i och med att detta fortfarande innebär att allt innehåll skjuts in i en array efter att ha sorterat innehållet först. Det tar bara ett "iterativt" tillvägagångssätt. Du kan koda ut detta för att skapa aggregeringspipeline för större poster, men bara att visa "två" borde visa att det inte är en riktigt bra idé att prova:

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

Och igen medan "möjligt" i tidigare versioner (detta använder 2.6 introducerade funktioner för att förkorta eftersom du redan taggar $$ROOT ), är de grundläggande stegen att lagra arrayen och sedan få varje objekt "ur stacken" med $first och jämföra det (och potentiellt andra) med objekt i arrayen för att ta bort dem och sedan ta bort "nästa först"-objektet från den högen tills ditt "översta N" slutligen är klart.

Slutsats

Tills dagen kommer att det finns en sådan operation som tillåter objekten i en $push aggregeringsackumulator för att begränsas till ett visst antal, då är detta egentligen inte en praktisk operation för aggregering.

Du kan göra det om data du har i dessa resultat är tillräckligt liten, och det kan till och med bara vara effektivare än bearbetningen på klientsidan om databasservrarna har tillräckliga specifikationer för att ge en verklig fördel. Men chansen är att ingetdera kommer att vara fallet i de flesta verkliga tillämpningar av rimlig användning.

Det bästa alternativet är att använda alternativet "parallell fråga" som visas först. Det kommer alltid att skalas bra, och det finns inget behov av att "koda runt" sådan logik att en viss gruppering kanske inte returnerar åtminstone de totala "top N" objekten som krävs och räkna ut hur man behåller dem (mycket längre exempel på det utelämnade ) eftersom den helt enkelt utför varje fråga och kombinerar resultaten.

Använd parallella frågor. Det kommer att bli bättre än det kodade tillvägagångssättet du har, och det kommer att överträffa det aggregeringssätt som visats långt. Tills det finns ett bättre alternativ åtminstone.



  1. En introduktion till Percona Server för MongoDB 4.2

  2. Få en distinkt aggregering av ett matrisfält över index

  3. Hitta största dokumentstorlek i MongoDB

  4. Percona Live Frankfurt 2018 - Sammanfattning av evenemanget och våra sessioner