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

Raggruppa valori e conteggi distinti per ciascuna proprietà in un'unica query

Esistono diversi approcci a seconda della versione disponibile, ma essenzialmente si scompongono tutti trasformando i campi del documento in documenti separati in un "array", quindi "svolgendo" quell'array con $unwind e facendo $group fasi per accumulare i totali e gli array di output.

MongoDB 3.4.4 e versioni successive

Le ultime versioni hanno operatori speciali come $arrayToObject e $objectToArray che può rendere il trasferimento all'"array" iniziale dal documento di origine più dinamico rispetto alle versioni precedenti:

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

Quindi, utilizzando $objectToArray trasformi il documento iniziale in una matrice delle sue chiavi e valori come "k" e "v" chiavi nella matrice di oggetti risultante. Applichiamo $filter qui per selezionare con "chiave". Qui usando $in con un elenco di chiavi che vogliamo, ma questo potrebbe essere utilizzato in modo più dinamico come un elenco di chiavi da "escludere" dove era più breve. Sta solo usando gli operatori logici per valutare la condizione.

La fase finale qui utilizza $replaceRoot e poiché tutte le nostre manipolazioni e "raggruppamenti" nel mezzo mantengono ancora quel "k" e "v" modulo, utilizziamo quindi $arrayToObject qui per promuovere la nostra "matrice di oggetti" in risultato alle "chiavi" del documento di livello superiore in output.

MongoDB 3.6 $mergeObjects

Come ulteriore ruga qui, MongoDB 3.6 include $mergeObjects che può essere utilizzato come "accumulatore " in un $group anche la fase di pipeline, sostituendo così $push e fare il $replaceRoot semplicemente spostando il "data" chiave alla "radice" del documento restituito invece:

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

Questo non è molto diverso da ciò che viene dimostrato nel complesso, ma dimostra semplicemente come $mergeObjects può essere utilizzato in questo modo e può essere utile nei casi in cui la chiave di raggruppamento era qualcosa di diverso e non volevamo quella "unione" finale allo spazio radice dell'oggetto.

Tieni presente che $arrayToObject è ancora necessario ritrasformare il "valore" nel nome della "chiave", ma lo facciamo solo durante l'accumulazione anziché dopo il raggruppamento, poiché la nuova accumulazione consente l'"unione" delle chiavi.

MongoDB 3.2

Riprendendo una versione o anche se hai un MongoDB 3.4.x inferiore alla versione 3.4.4, possiamo ancora usarne gran parte, ma invece ci occupiamo della creazione dell'array in modo più statico, pure come gestire la "trasformazione" finale sull'output in modo diverso a causa degli operatori di aggregazione che non abbiamo:

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

Questa è esattamente la stessa cosa, tranne per il fatto che invece di avere una trasformazione dinamica del documento nell'array, in realtà assegniamo "esplicitamente" ogni membro dell'array con lo stesso "k" e "v" notazione. A questo punto, sto davvero solo mantenendo quei nomi chiave per la convenzione poiché nessuno degli operatori di aggregazione qui dipende affatto da quello.

Inoltre, invece di utilizzare $replaceRoot , facciamo esattamente la stessa cosa che stava facendo la precedente implementazione della fase della pipeline, ma invece nel codice client. Tutti i driver MongoDB hanno alcune implementazioni di cursor.map() per abilitare le "trasformazioni del cursore". Qui con la shell utilizziamo le funzioni JavaScript di base di Array.map() e Array.reduce() per prendere quell'output e promuovere nuovamente il contenuto dell'array in modo che sia la chiave del documento di primo livello restituito.

MongoDB 2.6

E tornando a MongoDB 2.6 per coprire le versioni intermedie, l'unica cosa che cambia qui è l'utilizzo di $map e un $literal per l'input con la dichiarazione dell'array:

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

Poiché l'idea di base qui è quella di "iterare" una matrice fornita dei nomi dei campi, l'effettiva assegnazione dei valori avviene "annidando" il $cond dichiarazioni. Per tre possibili esiti ciò significa solo un singolo annidamento in modo da "ramificare" per ogni risultato.

Il moderno MongoDB dalla 3.4 ha $switch il che rende questa ramificazione più semplice, ma dimostra che la logica è sempre stata possibile e il $cond operator esiste da quando il framework di aggregazione è stato introdotto in MongoDB 2.2.

Ancora una volta, si applica la stessa trasformazione sul risultato del cursore poiché non c'è nulla di nuovo lì e la maggior parte dei linguaggi di programmazione ha la capacità di farlo per anni, se non dall'inizio.

Ovviamente il processo di base può essere eseguito anche a partire da MongoDB 2.2, ma semplicemente applicando la creazione dell'array e $unwind in un altro modo. Ma nessuno dovrebbe eseguire MongoDB sotto 2.8 in questo momento e il supporto ufficiale anche dalla 3.0 sta finendo rapidamente.

Uscita

Per la visualizzazione, l'output di tutte le pipeline dimostrate qui ha il seguente formato prima che venga eseguita l'ultima "trasformazione":

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

E poi da $replaceRoot o la trasformazione del cursore come mostrato il risultato diventa:

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

Quindi, mentre possiamo inserire alcuni nuovi e fantasiosi operatori nella pipeline di aggregazione dove abbiamo quelli disponibili, il caso d'uso più comune è in queste "trasformazioni di fine pipeline", nel qual caso possiamo anche semplicemente eseguire la stessa trasformazione su ogni documento in i risultati del cursore sono invece restituiti.