MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Gruppieren Sie unterschiedliche Werte und Anzahlen für jede Eigenschaft in einer Abfrage

Je nach verfügbarer Version gibt es unterschiedliche Ansätze, aber im Wesentlichen gliedern sie sich darin auf, Ihre Dokumentfelder in separate Dokumente in einem „Array“ umzuwandeln und dieses Array dann mit $unwind und sukzessive $group ausführen Phasen, um die Ausgangssummen und Arrays zu akkumulieren.

MongoDB 3.4.4 und höher

Neueste Versionen haben spezielle Operatoren wie $arrayToObject und $objectToArray was die Übertragung zum anfänglichen "Array" aus dem Quelldokument dynamischer machen kann als in früheren Versionen:

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

Verwenden Sie also $objectToArray Sie machen das ursprüngliche Dokument zu einem Array seiner Schlüssel und Werte als "k" und "v" Schlüssel in das resultierende Array von Objekten. Wir wenden $filter an hier um per "Taste" auszuwählen. Hier mit $in mit einer Liste von Schlüsseln, die wir wollen, aber dies könnte dynamischer als eine Liste von Schlüsseln verwendet werden, die "ausgeschlossen" werden sollen, wo dies kürzer war. Es werden nur logische Operatoren verwendet, um die Bedingung auszuwerten.

Die Endphase hier verwendet $replaceRoot und da all unsere Manipulationen und "Gruppierungen" dazwischen immer noch dieses "k" behalten und "v" Form verwenden wir dann $arrayToObject hier, um unser "Array von Objekten" im Ergebnis zu den "Schlüsseln" des Dokuments der obersten Ebene in der Ausgabe zu machen.

MongoDB 3.6 $mergeObjects

Als zusätzliches Problem enthält MongoDB 3.6 hier $mergeObjects die als "Akkumulator verwendet werden kann " in einem $group Pipeline-Stufe und ersetzt damit den $push und Erstellen des letzten $replaceRoot einfach die "data" verschieben Schlüssel stattdessen zum "Stamm" des zurückgegebenen Dokuments:

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

Dies unterscheidet sich nicht wirklich von dem, was insgesamt demonstriert wird, sondern zeigt einfach, wie $mergeObjects kann auf diese Weise verwendet werden und kann in Fällen nützlich sein, in denen der Gruppierungsschlüssel ein anderer war und wir diese endgültige "Verschmelzung" mit dem Stammbereich des Objekts nicht wollten.

Beachten Sie, dass $arrayToObject wird immer noch benötigt, um den "Wert" wieder in den Namen des "Schlüssels" umzuwandeln, aber wir tun dies nur während der Akkumulation und nicht nach der Gruppierung, da die neue Akkumulation das "Zusammenführen" von Schlüsseln ermöglicht.

MongoDB 3.2

Wenn wir eine Version zurücknehmen oder wenn Sie eine MongoDB 3.4.x haben, die kleiner als die Version 3.4.4 ist, können wir immer noch viel davon verwenden, aber stattdessen behandeln wir die Erstellung des Arrays auch auf statischere Weise da die endgültige "Transformation" bei der Ausgabe aufgrund der Aggregationsoperatoren, die wir nicht haben, anders behandelt wird:

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),{})
)

Das ist genau dasselbe, außer dass wir anstelle einer dynamischen Transformation des Dokuments in das Array tatsächlich jedem Array-Mitglied "explizit" denselben "k" zuweisen und "v" Notation. Ich behalte diese Schlüsselnamen an dieser Stelle wirklich nur für Konventionen bei, da keiner der Aggregationsoperatoren hier überhaupt davon abhängig ist.

Auch anstelle von $replaceRoot , machen wir genau das Gleiche wie die vorherige Implementierung der Pipeline-Phase dort, aber stattdessen im Client-Code. Alle MongoDB-Treiber haben eine Implementierung von cursor.map() um "Cursor-Transformationen" zu aktivieren. Hier verwenden wir mit der Shell die grundlegenden JavaScript-Funktionen von Array.map() und Array.reduce() um diese Ausgabe zu nehmen und den Inhalt des Arrays wieder zu den Schlüsseln des zurückgegebenen Dokuments der obersten Ebene zu machen.

MongoDB 2.6

Und wenn Sie auf MongoDB 2.6 zurückgreifen, um die Versionen dazwischen abzudecken, ändert sich hier nur die Verwendung von $map und ein $literal für die Eingabe mit der Array-Deklaration:

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),{})
)

Da die Grundidee hier darin besteht, ein bereitgestelltes Array der Feldnamen zu „iterieren“, erfolgt die eigentliche Zuweisung von Werten durch „Verschachtelung“ des $cond Aussagen. Für drei mögliche Ergebnisse bedeutet dies nur eine einzige Verschachtelung, um für jedes Ergebnis zu "verzweigen".

Moderne MongoDB ab 3.4 haben $switch was diese Verzweigung einfacher macht, aber dies zeigt, dass die Logik immer möglich war und der $cond Operator gibt es seit der Einführung des Aggregations-Frameworks in MongoDB 2.2.

Auch hier gilt die gleiche Transformation für das Cursor-Ergebnis, da es dort nichts Neues gibt und die meisten Programmiersprachen die Fähigkeit haben, dies jahrelang zu tun, wenn nicht von Anfang an.

Natürlich kann der grundlegende Prozess sogar weit zurück zu MongoDB 2.2 durchgeführt werden, aber nur die Array-Erstellung und $unwind auf andere Weise. Aber zu diesem Zeitpunkt sollte niemand MongoDB unter 2.8 ausführen, und die offizielle Unterstützung sogar von 3.0 geht schnell zur Neige.

Ausgabe

Zur Visualisierung hat die Ausgabe aller hier gezeigten Pipelines die folgende Form, bevor die letzte "Transformation" durchgeführt wird:

/* 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
                }
            ]
        }
    ]
}

Und dann entweder durch den $replaceRoot oder die Cursor-Transformation, wie gezeigt, das Ergebnis wird:

/* 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
        }
    ]
}

Während wir also einige neue und ausgefallene Operatoren in die Aggregationspipeline einfügen können, wo wir diese zur Verfügung haben, ist der häufigste Anwendungsfall diese "End-of-Pipeline-Transformationen". In diesem Fall können wir genauso gut einfach dieselbe Transformation für jedes Dokument durchführen stattdessen werden die Cursor-Ergebnisse zurückgegeben.