sql >> Database teknologi >  >> NoSQL >> MongoDB

mongodb grupperer værdier efter flere felter

TLDR-oversigt

I moderne MongoDB-udgivelser kan du brute force dette med $slice lige ved det grundlæggende aggregeringsresultat. For "store" resultater, kør i stedet parallelle forespørgsler for hver gruppering (en demonstrationsliste er i slutningen af ​​svaret), eller vent på, at SERVER-9377 løser sig, hvilket ville tillade en "begrænsning" for antallet af elementer til $push til et array.

db.books.aggregate([
    { "$group": {
        "_id": {
            "addr": "$addr",
            "book": "$book"
        },
        "bookCount": { "$sum": 1 }
    }},
    { "$group": {
        "_id": "$_id.addr",
        "books": { 
            "$push": { 
                "book": "$_id.book",
                "count": "$bookCount"
            },
        },
        "count": { "$sum": "$bookCount" }
    }},
    { "$sort": { "count": -1 } },
    { "$limit": 2 },
    { "$project": {
        "books": { "$slice": [ "$books", 2 ] },
        "count": 1
    }}
])
 

MongoDB 3.6 Preview

Løser stadig ikke SERVER-9377, men i denne udgivelse $lookup tillader en ny "ikke-korreleret" mulighed, som tager en "pipeline" udtryk som et argument i stedet for "localFields" og "foreignFields" muligheder. Dette tillader så en "selv-join" med et andet pipeline-udtryk, hvor vi kan anvende $limit for at returnere "top-n" resultaterne.

db.books.aggregate([
  { "$group": {
    "_id": "$addr",
    "count": { "$sum": 1 }
  }},
  { "$sort": { "count": -1 } },
  { "$limit": 2 },
  { "$lookup": {
    "from": "books",
    "let": {
      "addr": "$_id"
    },
    "pipeline": [
      { "$match": { 
        "$expr": { "$eq": [ "$addr", "$$addr"] }
      }},
      { "$group": {
        "_id": "$book",
        "count": { "$sum": 1 }
      }},
      { "$sort": { "count": -1  } },
      { "$limit": 2 }
    ],
    "as": "books"
  }}
])
 

Den anden tilføjelse her er naturligvis muligheden for at interpolere variablen gennem $expr ved hjælp af $match for at vælge de matchende elementer i "join", men den generelle forudsætning er en "pipeline i en pipeline", hvor det indre indhold kan filtreres efter matches fra forælderen. Da de begge selv er "pipelines", kan vi $limit hvert resultat separat.

Dette ville være den næstbedste mulighed for at køre parallelle forespørgsler, og det ville faktisk være bedre, hvis $match var tilladt og i stand til at bruge et indeks i "sub-pipeline"-behandlingen. Så som ikke bruger "grænsen til $push " som det refererede problem spørger om, leverer det faktisk noget, der burde fungere bedre.

Originalt indhold

Du ser ud til at være stødt på det øverste "N"-problem. På en måde er dit problem ret nemt at løse, dog ikke med den nøjagtige begrænsning, som du beder om:

db.books.aggregate([
    { "$group": {
        "_id": {
            "addr": "$addr",
            "book": "$book"
        },
        "bookCount": { "$sum": 1 }
    }},
    { "$group": {
        "_id": "$_id.addr",
        "books": { 
            "$push": { 
                "book": "$_id.book",
                "count": "$bookCount"
            },
        },
        "count": { "$sum": "$bookCount" }
    }},
    { "$sort": { "count": -1 } },
    { "$limit": 2 }
])
 

Det vil nu give dig et resultat som dette:

{ "result" : [ { "_id" : "address1", "books" : [ { "book" : "book4", "count" : 1 }, { "book" : "book5", "count" : 1 }, { "book" : "book1", "count" : 3 } ], "count" : 5 }, { "_id" : "address2", "books" : [ { "book" : "book5", "count" : 1 }, { "book" : "book1", "count" : 2 } ], "count" : 3 } ], "ok" : 1 }

Så dette adskiller sig fra det, du spørger om, da vi får de bedste resultater for adresseværdierne, er det underliggende valg af "bøger" ikke begrænset til kun en påkrævet mængde resultater.

Dette viser sig at være meget svært at gøre, men det kan lade sig gøre, selvom kompleksiteten blot øges med antallet af elementer, du skal matche. For at gøre det enkelt kan vi holde dette ved højst 2 kampe:

db.books.aggregate([
    { "$group": {
        "_id": {
            "addr": "$addr",
            "book": "$book"
        },
        "bookCount": { "$sum": 1 }
    }},
    { "$group": {
        "_id": "$_id.addr",
        "books": { 
            "$push": { 
                "book": "$_id.book",
                "count": "$bookCount"
            },
        },
        "count": { "$sum": "$bookCount" }
    }},
    { "$sort": { "count": -1 } },
    { "$limit": 2 },
    { "$unwind": "$books" },
    { "$sort": { "count": 1, "books.count": -1 } },
    { "$group": {
        "_id": "$_id",
        "books": { "$push": "$books" },
        "count": { "$first": "$count" }
    }},
    { "$project": {
        "_id": {
            "_id": "$_id",
            "books": "$books",
            "count": "$count"
        },
        "newBooks": "$books"
    }},
    { "$unwind": "$newBooks" },
    { "$group": {
      "_id": "$_id",
      "num1": { "$first": "$newBooks" }
    }},
    { "$project": {
        "_id": "$_id",
        "newBooks": "$_id.books",
        "num1": 1
    }},
    { "$unwind": "$newBooks" },
    { "$project": {
        "_id": "$_id",
        "num1": 1,
        "newBooks": 1,
        "seen": { "$eq": [
            "$num1",
            "$newBooks"
        ]}
    }},
    { "$match": { "seen": false } },
    { "$group":{
        "_id": "$_id._id",
        "num1": { "$first": "$num1" },
        "num2": { "$first": "$newBooks" },
        "count": { "$first": "$_id.count" }
    }},
    { "$project": {
        "num1": 1,
        "num2": 1,
        "count": 1,
        "type": { "$cond": [ 1, [true,false],0 ] }
    }},
    { "$unwind": "$type" },
    { "$project": {
        "books": { "$cond": [
            "$type",
            "$num1",
            "$num2"
        ]},
        "count": 1
    }},
    { "$group": {
        "_id": "$_id",
        "count": { "$first": "$count" },
        "books": { "$push": "$books" }
    }},
    { "$sort": { "count": -1 } }
])
 

Så det vil faktisk give dig de to øverste "bøger" fra de to øverste "adresse"-poster.

Men for mine penge skal du forblive med den første formular og derefter "skære" elementerne i arrayet, der returneres, for at tage de første "N"-elementer.

Demonstrationskode

Demonstrationskoden er passende til brug med aktuelle LTS-versioner af NodeJS fra v8.x- og v10.x-udgivelser. Det er mest for async/await syntaks, men der er intet i det generelle flow, der har en sådan begrænsning og tilpasser sig med små ændringer til almindelige løfter eller endda tilbage til almindelig callback-implementering.

index.js

const { MongoClient } = require('mongodb');
const fs = require('mz/fs');

const uri = 'mongodb://localhost:27017';

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {
    const client = await MongoClient.connect(uri);

    const db = client.db('bookDemo');
    const books = db.collection('books');

    let { version } = await db.command({ buildInfo: 1 });
    version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);

    // Clear and load books
    await books.deleteMany({});

    await books.insertMany(
      (await fs.readFile('books.json'))
        .toString()
        .replace(/\n$/,"")
        .split("\n")
        .map(JSON.parse)
    );

    if ( version >= 3.6 ) {

    // Non-correlated pipeline with limits
      let result = await books.aggregate([
        { "$group": {
          "_id": "$addr",
          "count": { "$sum": 1 }
        }},
        { "$sort": { "count": -1 } },
        { "$limit": 2 },
        { "$lookup": {
          "from": "books",
          "as": "books",
          "let": { "addr": "$_id" },
          "pipeline": [
            { "$match": {
              "$expr": { "$eq": [ "$addr", "$$addr" ] }
            }},
            { "$group": {
              "_id": "$book",
              "count": { "$sum": 1 },
            }},
            { "$sort": { "count": -1 } },
            { "$limit": 2 }
          ]
        }}
      ]).toArray();

      log({ result });
    }

    // Serial result procesing with parallel fetch

    // First get top addr items
    let topaddr = await books.aggregate([
      { "$group": {
        "_id": "$addr",
        "count": { "$sum": 1 }
      }},
      { "$sort": { "count": -1 } },
      { "$limit": 2 }
    ]).toArray();

    // Run parallel top books for each addr
    let topbooks = await Promise.all(
      topaddr.map(({ _id: addr }) =>
        books.aggregate([
          { "$match": { addr } },
          { "$group": {
            "_id": "$book",
            "count": { "$sum": 1 }
          }},
          { "$sort": { "count": -1 } },
          { "$limit": 2 }
        ]).toArray()
      )
    );

    // Merge output
    topaddr = topaddr.map((d,i) => ({ ...d, books: topbooks[i] }));
    log({ topaddr });

    client.close();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()
 

books.json

{ "addr": "address1",  "book": "book1"  }
{ "addr": "address2",  "book": "book1"  }
{ "addr": "address1",  "book": "book5"  }
{ "addr": "address3",  "book": "book9"  }
{ "addr": "address2",  "book": "book5"  }
{ "addr": "address2",  "book": "book1"  }
{ "addr": "address1",  "book": "book1"  }
{ "addr": "address15", "book": "book1"  }
{ "addr": "address9",  "book": "book99" }
{ "addr": "address90", "book": "book33" }
{ "addr": "address4",  "book": "book3"  }
{ "addr": "address5",  "book": "book1"  }
{ "addr": "address77", "book": "book11" }
{ "addr": "address1",  "book": "book1"  }
 


  1. Robust meddelelsesserialisering i Apache Kafka ved hjælp af Apache Avro, del 1

  2. MongoDB-sammenlægning på Loopback

  3. Kan ikke starte MongoDB som en tjeneste

  4. Sådan implementerer du ClusterControl på AWS for at administrere din clouddatabase