sql >> Databasteknik >  >> NoSQL >> MongoDB

Gruppera distinkta värden och antal för varje egenskap i en fråga

Det finns olika tillvägagångssätt beroende på vilken version som är tillgänglig, men de går i huvudsak upp till att omvandla dina dokumentfält till separata dokument i en "array", sedan "avveckla" den matrisen med $unwind och gör successivt $group steg för att ackumulera utdatasummor och matriser.

MongoDB 3.4.4 och senare

De senaste utgåvorna har speciella operatorer som $arrayToObject och $objectToArray vilket kan göra överföringen till den initiala "arrayen" från källdokumentet mer dynamisk än i tidigare utgåvor:

db.profile.aggregate([
  { "$project": { 
     "_id": 0,
     "data": { 
       "$filter": {
         "input": { "$objectToArray": "$$ROOT" },
         "cond": { "$in": [ "$$this.k", ["gender","caste","education"] ] }
       }   
     }
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": "$data",
    "total": { "$sum": 1 }  
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": "$data"
    }
  }}
])

Så använder $objectToArray du gör det initiala dokumentet till en uppsättning av dess nycklar och värden som "k" och "v" nycklar i den resulterande uppsättningen av objekt. Vi tillämpar $filter här för att välja med "knapp". Här använder du $in med en lista över nycklar som vi vill ha, men detta skulle kunna användas mer dynamiskt som en lista med nycklar för att "utesluta" där det var kortare. Det är bara att använda logiska operatorer för att utvärdera villkoret.

Slutsteget här använder $replaceRoot och eftersom all vår manipulation och "gruppering" däremellan fortfarande behåller det "k" och "v" formulär använder vi sedan $arrayToObject här för att marknadsföra vår "array av objekt" som resultat till "nycklarna" för dokumentet på högsta nivån i utdata.

MongoDB 3.6 $mergeObjects

Som en extra rynka här inkluderar MongoDB 3.6 $mergeObjects som kan användas som en "ackumulator " i en $group pipeline-steget också, vilket ersätter $push och gör den sista $replaceRoot helt enkelt flytta "data" nyckeln till "roten" av det returnerade dokumentet istället:

db.profile.aggregate([
  { "$project": { 
     "_id": 0,
     "data": { 
       "$filter": {
         "input": { "$objectToArray": "$$ROOT" },
         "cond": { "$in": [ "$$this.k", ["gender","caste","education"] ] }
       }   
     }
  }},
  { "$unwind": "$data" },
  { "$group": { "_id": "$data", "total": { "$sum": 1 } }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": {
      "$mergeObjects": {
        "$arrayToObject": [
          [{ "k": "$_id", "v": "$v" }]
        ] 
      }
    }  
  }},
  { "$replaceRoot": { "newRoot": "$data"  } }
])

Detta är egentligen inte så annorlunda mot vad som demonstreras överlag, utan visar helt enkelt hur $mergeObjects kan användas på det här sättet och kan vara användbart i de fall där grupperingsnyckeln var något annorlunda och vi inte ville ha den slutliga "sammanslagningen" till objektets rotutrymme.

Observera att $arrayToObject behövs fortfarande för att omvandla "värdet" tillbaka till namnet på "nyckeln", men vi gör det bara under ackumuleringen snarare än efter grupperingen, eftersom den nya ackumuleringen tillåter "sammanslagning" av nycklar.

MongoDB 3.2

Om du tar tillbaka en version eller till och med om du har en MongoDB 3.4.x som är mindre än 3.4.4-versionen, kan vi fortfarande använda mycket av detta men istället hanterar vi skapandet av arrayen på ett mer statiskt sätt också eftersom vi hanterar den slutliga "transformen" på utdata annorlunda på grund av de aggregeringsoperatorer som vi inte har:

db.profile.aggregate([
  { "$project": {
    "data": [
      { "k": "gender", "v": "$gender" },
      { "k": "caste", "v": "$caste" },
      { "k": "education", "v": "$education" }
    ]
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": "$data",
    "total": { "$sum": 1 }  
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  /*
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": "$data"
    }
  }}
  */
]).map( d => 
  d.data.map( e => ({ [e.k]: e.v }) )
    .reduce((acc,curr) => Object.assign(acc,curr),{})
)

Detta är exakt samma sak, förutom att istället för att ha en dynamisk transformation av dokumentet till arrayen, tilldelar vi faktiskt varje arraymedlem samma "k" "explicit" och "v" notation. Behåller egentligen bara dessa nyckelnamn för konventionen vid det här laget eftersom ingen av aggregeringsoperatörerna här är beroende av det alls.

Också istället för att använda $replaceRoot , vi gör precis samma sak som vad den tidigare implementeringen av pipelinestadiet gjorde där, men i klientkoden istället. Alla MongoDB-drivrutiner har någon implementering av cursor.map() för att aktivera "marköromvandlingar". Här med skalet använder vi de grundläggande JavaScript-funktionerna i Array.map() och Array.reduce() för att ta den utmatningen och återigen främja arrayinnehållet till att vara nycklarna för dokumentet på högsta nivån.

MongoDB 2.6

Och faller tillbaka till MongoDB 2.6 för att täcka versionerna däremellan, det enda som ändras här är användningen av $map och en $literal för inmatning med matrisdeklarationen:

db.profile.aggregate([
  { "$project": {
    "data": {
      "$map": {
        "input": { "$literal": ["gender","caste", "education"] },
        "as": "k",
        "in": {
          "k": "$$k",
          "v": {
            "$cond": {
              "if": { "$eq": [ "$$k", "gender" ] },
              "then": "$gender",
              "else": {
                "$cond": {
                  "if": { "$eq": [ "$$k", "caste" ] },
                  "then": "$caste",
                  "else": "$education"
                }
              }    
            }
          }    
        }
      }
    }
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": "$data",
    "total": { "$sum": 1 }  
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  /*
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": "$data"
    }
  }}
  */
])
.map( d => 
  d.data.map( e => ({ [e.k]: e.v }) )
    .reduce((acc,curr) => Object.assign(acc,curr),{})
)

Eftersom grundidén här är att "iterera" en tillhandahållen uppsättning av fältnamnen, kommer den faktiska tilldelningen av värden genom att "kapsla" $cond uttalanden. För tre möjliga utfall betyder detta bara en enda kapsling för att "grena" för varje resultat.

Modern MongoDB från 3.4 har $switch vilket gör denna förgrening enklare, men detta visar att logiken alltid var möjlig och $cond operatören har funnits sedan aggregeringsramverket introducerades i MongoDB 2.2.

Återigen gäller samma transformation på markörresultatet eftersom det inte finns något nytt där och de flesta programmeringsspråk har förmågan att göra detta i flera år, om inte från början.

Naturligtvis kan den grundläggande processen till och med göras långt tillbaka till MongoDB 2.2, men bara genom att använda arrayskapandet och $unwind på ett annat sätt. Men ingen borde köra någon MongoDB under 2.8 vid denna tidpunkt, och officiellt stöd även från 3.0 håller på att ta slut.

Utdata

För visualisering har utdata från alla demonstrerade pipelines här följande form innan den sista "transformen" görs:

/* 1 */
{
    "_id" : null,
    "data" : [ 
        {
            "k" : "gender",
            "v" : [ 
                {
                    "name" : "Male",
                    "total" : 3.0
                }, 
                {
                    "name" : "Female",
                    "total" : 2.0
                }
            ]
        }, 
        {
            "k" : "education",
            "v" : [ 
                {
                    "name" : "M.C.A",
                    "total" : 1.0
                }, 
                {
                    "name" : "B.E",
                    "total" : 3.0
                }, 
                {
                    "name" : "B.Com",
                    "total" : 1.0
                }
            ]
        }, 
        {
            "k" : "caste",
            "v" : [ 
                {
                    "name" : "Lingayath",
                    "total" : 3.0
                }, 
                {
                    "name" : "Vokkaliga",
                    "total" : 2.0
                }
            ]
        }
    ]
}

Och sedan antingen genom $replaceRoot eller markörtransformeringen som visas blir resultatet:

/* 1 */
{
    "gender" : [ 
        {
            "name" : "Male",
            "total" : 3.0
        }, 
        {
            "name" : "Female",
            "total" : 2.0
        }
    ],
    "education" : [ 
        {
            "name" : "M.C.A",
            "total" : 1.0
        }, 
        {
            "name" : "B.E",
            "total" : 3.0
        }, 
        {
            "name" : "B.Com",
            "total" : 1.0
        }
    ],
    "caste" : [ 
        {
            "name" : "Lingayath",
            "total" : 3.0
        }, 
        {
            "name" : "Vokkaliga",
            "total" : 2.0
        }
    ]
}

Så även om vi kan lägga in några nya och snygga operatörer i aggregeringspipelinen där vi har de tillgängliga, är det vanligaste användningsfallet i dessa "end of pipeline-transformeringar", i vilket fall vi lika gärna kan göra samma transformation på varje dokument i markörresultaten returnerades istället.




  1. Redis-nycklar visas inte när du använder Cache-fasad i Laravel

  2. hur man importerar en .csv-datafil till Redis-databasen

  3. Mongodb - dubbletter av fält i $set och $setOnInsert

  4. Skicka meddelanden till grupper i Django Channels 2