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

Aggregation Akkumuler indre objekter

Som en hurtig bemærkning skal du ændre din "værdi" feltet inde i "værdier" at være numerisk, da det i øjeblikket er en streng. Men videre til svaret:

Hvis du har adgang til $reduce fra MongoDB 3.4, så kan du faktisk gøre sådan noget:

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" }
         }
       }
     }
  }}
])

Hvis du har MongoDB 3.6, kan du rydde lidt op i det 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 er mere eller mindre det samme, bortset fra at vi beholder additionalData

Går vi lidt tilbage før det, så kan du altid $unwind "byer" at akkumulere:

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" }
         }
       }
     }
  }}
])

Alle returnerer (næsten) det samme:

{
        "_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 to første formularer er selvfølgelig den mest optimale ting at gøre, da de simpelthen arbejder "inden for" det samme dokument til enhver tid.

Operatører som $reduce tillade "akkumulation"-udtryk på arrays, så vi kan bruge det her til at beholde et "reduceret" array, som vi tester for det unikke "_id" værdi ved hjælp af $indexOfArray for at se, om der allerede er en akkumuleret vare, der matcher. Et resultat af -1 betyder, at den ikke er der.

For at konstruere et "reduceret array" tager vi "initialValue" af [] som et tomt array og derefter tilføje til det via $concatArrays . Hele denne proces afgøres via den "ternære" $cond operator, der betragter "hvis" betingelse og "derefter" enten "joins" outputtet af $filter på den aktuelle $$værdi for at ekskludere det aktuelle indeks _id indgang, med selvfølgelig en anden "array", der repræsenterer entalsobjektet.

Til dette "objekt" bruger vi igen $indexOfArray for faktisk at få det matchede indeks, da vi ved, at elementet "er der", og bruge det til at udtrække den aktuelle "besøgte" værdi fra denne post via $arrayElemAt og $add til det for at stige.

I "else" I tilfælde af at vi blot tilføjer et "array" som et "objekt", som bare har en standard "visited" værdi af 1 . Brug af begge disse tilfælde akkumulerer effektivt unikke værdier i arrayet til output.

I sidstnævnte version er vi bare $unwind arrayet og brug successive $group trin for først at "tælle" på de unikke indre indgange og derefter "rekonstruere arrayet" til den lignende form.

Brug af $unwind ser langt mere simpelt ud, men da det faktisk gør, er at tage en kopi af dokumentet for hver array-indgang, så tilføjer dette faktisk betydelige overhead til behandlingen. I moderne versioner er der generelt array-operatorer, som betyder, at du ikke behøver at bruge dette, medmindre din hensigt er at "akkumulere på tværs af dokumenter". Så hvis du rent faktisk har brug for at $group på en værdi af en nøgle fra "inde i" et array, så er det der, du faktisk skal bruge det.

Med hensyn til "variabler" så kan vi blot bruge $filter igen her for at få den matchende "Budget" indgang. Vi gør dette som input til $map operator, som tillader "omformning" af array-indholdet. Det ønsker vi primært, så du kan tage indholdet af "værdierne" (når du har gjort det hele numerisk) og brug $avg operator, som leveres, at "feltstinotation" danner direkte til matrixværdierne, fordi den faktisk kan returnere et resultat fra et sådant input.

Det gør generelt rundturen til stort set ALLE de vigtigste "array-operatører" for aggregeringspipelinen (undtagen "set"-operatørerne) alle inden for et enkelt pipeline-stadium.

Glem heller aldrig, at du næsten altid vil $match med almindelige Forespørgselsoperatører som den "allerførste fase" af enhver aggregeringspipeline for blot at vælge de dokumenter, du har brug for. Ideelt set ved at bruge et indeks.

Alternativt

Suppleanter arbejder gennem dokumenterne i klientkoden. Det vil generelt ikke anbefales, da alle metoder ovenfor viser, at de faktisk "reducerer" indholdet som returneret fra serveren, som det generelt er meningen med "serversammenlægninger".

Det "kan" være muligt på grund af den "dokumentbaserede" natur, at større resultatsæt kan tage betydeligt længere tid ved at bruge $unwind og klientbehandling kunne være en mulighed, men jeg ville betragte det som langt mere sandsynligt

Nedenfor er en liste, der demonstrerer anvendelse af en transformation til markørstrømmen, efterhånden som resultater returneres ved at gøre det samme. Der er tre demonstrerede versioner af transformationen, der viser "præcis" den samme logik som ovenfor, en implementering med lodash metoder til akkumulering og en "naturlig" akkumulering på Map 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. Optimering - find på alle felter i Mongoose MongoDB skema

  2. Mongoose udfylde sub-sub dokument

  3. Kan ikke godkendes til mongo, godkendelse mislykkes

  4. fagforening på samme samling i mongodb