sql >> Databasteknik >  >> NoSQL >> MongoDB

Aggregation Ackumulera inre objekt

Som en snabb notering måste du ändra ditt "värde" fältet i "värden" att vara numerisk, eftersom det för närvarande är en sträng. Men till svaret:

Om du har tillgång till $reduce från MongoDB 3.4, så kan du faktiskt göra något så här:

db.collection.aggregate([
  { "$addFields": {
     "cities": {
       "$reduce": {
         "input": "$cities",
         "initialValue": [],
         "in": {
           "$cond": {
             "if": { "$ne": [{ "$indexOfArray": ["$$value._id", "$$this._id"] }, -1] },
             "then": {
               "$concatArrays": [
                 { "$filter": {
                   "input": "$$value",
                   "as": "v",
                   "cond": { "$ne": [ "$$this._id", "$$v._id" ] }
                 }},
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": {
                     "$add": [
                       { "$arrayElemAt": [
                         "$$value.visited",
                         { "$indexOfArray": [ "$$value._id", "$$this._id" ] }
                       ]},
                       1
                     ]
                   }
                 }]
               ]
             },
             "else": {
               "$concatArrays": [
                 "$$value",
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": 1
                 }]
               ]
             }
           }
         }
       }
     },
     "variables": {
       "$map": {
         "input": {
           "$filter": {
             "input": "$variables",
             "cond": { "$eq": ["$$this.name", "Budget"] } 
           }
         },
         "in": {
           "_id": "$$this._id",
           "name": "$$this.name",
           "defaultValue": "$$this.defaultValue",
           "lastValue": "$$this.lastValue",
           "value": { "$avg": "$$this.values.value" }
         }
       }
     }
  }}
])

Om du har MongoDB 3.6 kan du rensa upp det lite med $mergeObjects :

db.collection.aggregate([
  { "$addFields": {
     "cities": {
       "$reduce": {
         "input": "$cities",
         "initialValue": [],
         "in": {
           "$cond": {
             "if": { "$ne": [{ "$indexOfArray": ["$$value._id", "$$this._id"] }, -1] },
             "then": {
               "$concatArrays": [
                 { "$filter": {
                   "input": "$$value",
                   "as": "v",
                   "cond": { "$ne": [ "$$this._id", "$$v._id" ] }
                 }},
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": {
                     "$add": [
                       { "$arrayElemAt": [
                         "$$value.visited",
                         { "$indexOfArray": [ "$$value._id", "$$this._id" ] }
                       ]},
                       1
                     ]
                   }
                 }]
               ]
             },
             "else": {
               "$concatArrays": [
                 "$$value",
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": 1
                 }]
               ]
             }
           }
         }
       }
     },
     "variables": {
       "$map": {
         "input": {
           "$filter": {
             "input": "$variables",
             "cond": { "$eq": ["$$this.name", "Budget"] } 
           }
         },
         "in": {
           "$mergeObjects": [
             "$$this",
             { "values": { "$avg": "$$this.values.value" } }
           ]
         }
       }
     }
  }}
])

Men det är mer eller mindre samma sak förutom att vi behåller additionalData

Om du går tillbaka lite innan dess kan du alltid $unwind "städer" att ackumulera:

db.collection.aggregate([
  { "$unwind": "$cities" },
  { "$group": {
     "_id": { 
       "_id": "$_id",
       "cities": {
         "_id": "$cities._id",
         "name": "$cities.name"
       }
     },
     "_class": { "$first": "$class" },
     "name": { "$first": "$name" },
     "startTimestamp": { "$first": "$startTimestamp" },
     "endTimestamp" : { "$first": "$endTimestamp" },
     "source" : { "$first": "$source" },
     "variables": { "$first": "$variables" },
     "visited": { "$sum": 1 }
  }},
  { "$group": {
     "_id": "$_id._id",
     "_class": { "$first": "$class" },
     "name": { "$first": "$name" },
     "startTimestamp": { "$first": "$startTimestamp" },
     "endTimestamp" : { "$first": "$endTimestamp" },
     "source" : { "$first": "$source" },
     "cities": {
       "$push": {
         "_id": "$_id.cities._id",
         "name": "$_id.cities.name",
         "visited": "$visited"
       }
     },
     "variables": { "$first": "$variables" },
  }},
  { "$addFields": {
     "variables": {
       "$map": {
         "input": {
           "$filter": {
             "input": "$variables",
             "cond": { "$eq": ["$$this.name", "Budget"] } 
           }
         },
         "in": {
           "_id": "$$this._id",
           "name": "$$this.name",
           "defaultValue": "$$this.defaultValue",
           "lastValue": "$$this.lastValue",
           "value": { "$avg": "$$this.values.value" }
         }
       }
     }
  }}
])

Alla returnerar (nästan) samma sak:

{
        "_id" : ObjectId("5afc2f06e1da131c9802071e"),
        "_class" : "Traveler",
        "name" : "John Due",
        "startTimestamp" : 1526476550933,
        "endTimestamp" : 1526476554823,
        "source" : "istanbul",
        "cities" : [
                {
                        "_id" : "ef8f6b26328f-0663202f94faeaeb-1122",
                        "name" : "Cairo",
                        "visited" : 1
                },
                {
                        "_id" : "ef8f6b26328f-0663202f94faeaeb-3981",
                        "name" : "Moscow",
                        "visited" : 2
                }
        ],
        "variables" : [
                {
                        "_id" : "c8103687c1c8-97d749e349d785c8-9154",
                        "name" : "Budget",
                        "defaultValue" : "",
                        "lastValue" : "",
                        "value" : 3000
                }
        ]
}

De två första formerna är naturligtvis det mest optimala att göra eftersom de helt enkelt arbetar "inom" samma dokument hela tiden.

Operatörer som $reduce tillåt "ackumulation"-uttryck på arrayer, så vi kan använda det här för att behålla en "reducerad" array som vi testar för det unika "_id" värde med $indexOfArray för att se om det redan finns en ackumulerad artikel som matchar. Ett resultat av -1 betyder att den inte finns där.

För att konstruera en "reducerad array" tar vi "initialValue" av [] som en tom array och lägg sedan till den via $concatArrays . Hela den processen avgörs via den "ternära" $cond operator som beaktar "if" condition och "då" antingen "ansluter sig" till utdata från $filter på det aktuella $$värdet för att utesluta det aktuella indexet _id post, med naturligtvis en annan "array" som representerar singularobjektet.

För det "objektet" använder vi återigen $indexOfArray för att faktiskt få det matchade indexet eftersom vi vet att objektet "finns där", och använda det för att extrahera den nuvarande "besökt" värde från den posten via $arrayElemAt och $add till den för att öka.

I "annat" I detta fall lägger vi helt enkelt till en "array" som ett "objekt" som bara har en standard "besökt" värdet 1 . Genom att använda båda dessa fall ackumuleras effektivt unika värden inom arrayen för utmatning.

I den senare versionen är vi bara $unwind arrayen och använd successivt $group steg för att först "räkna" på de unika inre posterna och sedan "rekonstruera arrayen" till liknande form.

Använda $unwind ser mycket enklare ut, men eftersom vad det faktiskt gör är att ta en kopia av dokumentet för varje array-post, så lägger detta faktiskt till avsevärd overhead till bearbetningen. I moderna versioner finns det generellt arrayoperatorer som innebär att du inte behöver använda detta om inte din avsikt är att "ackumulera över dokument". Så om du faktiskt behöver $group på ett värde på en nyckel från "inuti" en array, så är det där du faktiskt behöver använda den.

När det gäller "variabler" då kan vi helt enkelt använda $filter här igen för att få den matchande "Budget" inträde. Vi gör detta som indata till $map operatör som tillåter "omformning" av arrayinnehållet. Vi vill främst ha det så att du kan ta innehållet i "värdena" (när du har gjort allt numeriskt) och använd $avg operator, som tillhandahålls att "fältvägsnotation" bildar direkt till matrisvärdena eftersom den faktiskt kan returnera ett resultat från en sådan inmatning.

Det gör i allmänhet att i stort sett ALLA de viktigaste "array-operatörerna" för aggregeringspipelinen (exklusive "set"-operatörerna) alla inom ett enda pipelinesteg.

Glöm heller aldrig att du nästan alltid vill $match med vanliga Frågeoperatörer som det "allra första steget" i en aggregeringspipeline för att bara välja de dokument du behöver. Helst använder du ett index.

Alternativ

Suppleanter arbetar igenom dokumenten i klientkoden. Det skulle i allmänhet inte rekommenderas eftersom alla metoder ovan visar att de faktiskt "minskar" innehållet som returneras från servern, vilket i allmänhet är poängen med "serveraggregationer".

Det "kan" vara möjligt på grund av den "dokumentbaserade" naturen att större resultatuppsättningar kan ta betydligt längre tid med $unwind och klientbearbetning kan vara ett alternativ, men jag anser att det är mycket mer troligt

Nedan finns en lista som visar att en transformering tillämpas på markörströmmen när resultat returneras och gör samma sak. Det finns tre demonstrerade versioner av transformationen, som visar "exakt" samma logik som ovan, en implementering med lodash metoder för ackumulering och en "naturlig" ackumulering på Kartan implementering:

const { MongoClient } = require('mongodb');
const { chain } = require('lodash');

const uri = 'mongodb://localhost:27017';
const opts = { useNewUrlParser: true };

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

const transform = ({ cities, variables, ...d }) => ({
  ...d,
  cities: cities.reduce((o,{ _id, name }) =>
    (o.map(i => i._id).indexOf(_id) != -1)
      ? [
          ...o.filter(i => i._id != _id),
          { _id, name, visited: o.find(e => e._id === _id).visited + 1 }
        ]
      : [ ...o, { _id, name, visited: 1 } ]
  , []).sort((a,b) => b.visited - a.visited),
  variables: variables.filter(v => v.name === "Budget")
    .map(({ values, additionalData, ...v }) => ({
      ...v,
      values: (values != undefined)
        ? values.reduce((o,e) => o + e.value, 0) / values.length
        : 0
    }))
});

const alternate = ({ cities, variables, ...d }) => ({
  ...d,
  cities: chain(cities)
    .groupBy("_id")
    .toPairs()
    .map(([k,v]) =>
      ({
        ...v.reduce((o,{ _id, name }) => ({ ...o, _id, name }),{}),
        visited: v.length
      })
    )
    .sort((a,b) => b.visited - a.visited)
    .value(),
  variables: variables.filter(v => v.name === "Budget")
    .map(({ values, additionalData, ...v }) => ({
      ...v,
      values: (values != undefined)
        ? values.reduce((o,e) => o + e.value, 0) / values.length
        : 0
    }))

});

const natural = ({ cities, variables, ...d }) => ({
  ...d,
  cities: [
    ...cities
      .reduce((o,{ _id, name }) => o.set(_id,
        [ ...(o.has(_id) ? o.get(_id) : []), { _id, name } ]), new Map())
      .entries()
  ]
  .map(([k,v]) =>
    ({
      ...v.reduce((o,{ _id, name }) => ({ ...o, _id, name }),{}),
      visited: v.length
    })
  )
  .sort((a,b) => b.visited - a.visited),
  variables: variables.filter(v => v.name === "Budget")
    .map(({ values, additionalData, ...v }) => ({
      ...v,
      values: (values != undefined)
        ? values.reduce((o,e) => o + e.value, 0) / values.length
        : 0
    }))

});

(async function() {

  try {

    const client = await MongoClient.connect(uri, opts);

    let db = client.db('test');
    let coll = db.collection('junk');

    let cursor = coll.find().map(natural);

    while (await cursor.hasNext()) {
      let doc = await cursor.next();
      log(doc);
    }

    client.close();

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

})()



  1. MongoDB $ifNull

  2. Enkel anslutning till mongodb i react-appen

  3. GridFS i Spring Data MongoDB

  4. Fråga MongoDB-sökning endast under specifika tider