MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Regrouper des valeurs et des nombres distincts pour chaque propriété dans une seule requête

Il existe différentes approches selon la version disponible, mais elles se résument toutes essentiellement à transformer vos champs de document en documents séparés dans un "tableau", puis à "dérouler" ce tableau avec $unwind et en faisant successivement $group étapes afin d'accumuler les totaux et les tableaux de sortie.

MongoDB 3.4.4 et supérieur

Les dernières versions ont des opérateurs spéciaux comme $arrayToObject et $objectToArray ce qui peut rendre le transfert vers le "tableau" initial à partir du document source plus dynamique que dans les versions précédentes :

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

Donc, en utilisant $objectToArray vous transformez le document initial en un tableau de ses clés et valeurs sous la forme "k" et "v" clés dans le tableau d'objets résultant. Nous appliquons $filter ici pour sélectionner par "clé". Ici en utilisant $in avec une liste de clés que nous voulons, mais cela pourrait être utilisé de manière plus dynamique comme une liste de clés à "exclure" là où c'était plus court. Il utilise simplement des opérateurs logiques pour évaluer la condition.

L'étape finale utilise ici $replaceRoot et puisque toutes nos manipulations et "regroupements" entre les deux conservent toujours ce "k" et "v" form, nous utilisons ensuite $arrayToObject ici pour promouvoir notre "tableau d'objets" dans le résultat aux "clés" du document de niveau supérieur dans la sortie.

MongoDB 3.6 $mergeObjects

Comme une ride supplémentaire ici, MongoDB 3.6 inclut $mergeObjects qui peut être utilisé comme un "accumulateur " dans un $group l'étape du pipeline également, remplaçant ainsi le $push et faire le dernier $replaceRoot en déplaçant simplement les "data" key à la "racine" du document renvoyé à la place :

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

Ce n'est pas vraiment différent de ce qui est démontré dans l'ensemble, mais montre simplement comment $mergeObjects peut être utilisé de cette manière et peut être utile dans les cas où la clé de regroupement était quelque chose de différent et nous ne voulions pas cette "fusion" finale dans l'espace racine de l'objet.

Notez que le $arrayToObject est toujours nécessaire de retransformer la "valeur" en nom de la "clé", mais on le fait juste pendant l'accumulation plutôt qu'après le regroupement, puisque la nouvelle accumulation permet la "fusion" des clés.

MongoDB 3.2

En reprenant une version ou même si vous avez un MongoDB 3.4.x inférieur à la version 3.4.4, nous pouvons toujours en utiliser une grande partie, mais nous traitons également la création du tableau de manière plus statique. comme gérer la "transformation" finale sur la sortie différemment en raison des opérateurs d'agrégation que nous n'avons pas :

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

C'est exactement la même chose, sauf qu'au lieu d'avoir une transformation dynamique du document dans le tableau, nous attribuons "explicitement" chaque membre du tableau avec le même "k" et "v" notation. Vraiment juste en gardant ces noms de clé pour la convention à ce stade car aucun des opérateurs d'agrégation ici ne dépend de cela.

Aussi au lieu d'utiliser $replaceRoot , nous faisons exactement la même chose que ce que faisait l'implémentation de l'étape de pipeline précédente, mais dans le code client à la place. Tous les pilotes MongoDB ont une implémentation de cursor.map() pour activer les "transformations du curseur". Ici, avec le shell, nous utilisons les fonctions JavaScript de base de Array.map() et Array.reduce() pour prendre cette sortie et promouvoir à nouveau le contenu du tableau en tant que clés du document de niveau supérieur renvoyé.

MongoDB 2.6

Et revenant à MongoDB 2.6 pour couvrir les versions intermédiaires, la seule chose qui change ici est l'utilisation de $map et un $literal pour l'entrée avec la déclaration de tableau :

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

Étant donné que l'idée de base ici est "d'itérer" un tableau fourni des noms de champs, l'affectation réelle des valeurs vient en "emboîtant" le $cond déclarations. Pour trois résultats possibles, cela signifie une seule imbrication afin de "se ramifier" pour chaque résultat.

MongoDB moderne de 3.4 a $switch ce qui simplifie cette ramification, mais cela démontre que la logique était toujours possible et le $cond L'opérateur existe depuis l'introduction du cadre d'agrégation dans MongoDB 2.2.

Encore une fois, la même transformation sur le résultat du curseur s'applique car il n'y a rien de nouveau et la plupart des langages de programmation ont la capacité de le faire pendant des années, voire depuis le début.

Bien sûr, le processus de base peut même être effectué depuis MongoDB 2.2, mais en appliquant simplement la création de tableau et $unwind d'une autre façon. Mais personne ne devrait exécuter MongoDB sous 2.8 à ce stade, et le support officiel, même à partir de 3.0, s'épuise même rapidement.

Sortie

Pour la visualisation, la sortie de tous les pipelines démontrés ici a la forme suivante avant que la dernière "transformation" ne soit effectuée :

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

Et ensuite soit par le $replaceRoot ou la transformation du curseur comme démontré, le résultat devient :

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

Ainsi, bien que nous puissions mettre de nouveaux opérateurs sophistiqués dans le pipeline d'agrégation lorsque nous en avons, le cas d'utilisation le plus courant est dans ces "transformations de fin de pipeline", auquel cas nous pouvons tout aussi bien effectuer la même transformation sur chaque document dans les résultats du curseur sont renvoyés à la place.