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

Query aggregata Mongodb o troppo complessa?

Anche se avrebbe dovuto essere più chiaro nella tua domanda, il tuo campione di output dalla fonte suggerisce che stai cercando:

  • Conteggio totale di messaggi per "uid"
  • Conteggio distinto di valori in "a"
  • Conteggio distinto di valori in "da"
  • Riepilogo dei conteggi per "ora" per ogni "uid"

Tutto ciò è possibile in un'unica istruzione di aggregazione e richiede solo un'attenta gestione degli elenchi distinti e quindi alcune manipolazioni per mappare i risultati per ogni ora in un periodo di 24 ore.

L'approccio migliore qui è aiutato dagli operatori introdotti in MongoDB 3.2:

db.collection.aggregate([
    // First group by hour within "uid" and keep distinct "to" and "from"
    { "$group": {
        "_id": {
            "uid": "$uid",
            "time": { "$hour": "$timestamp" }
        },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "count": { "$sum": 1 }
    }},

    // Roll-up to "uid" and keep each hour in an array
    { "$group": {
        "_id": "$_id.uid",
        "total": { "$sum": "$count" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { 
            "$push": {
                "index": "$_id.time",
                "count": "$count"
            }
        }
     }},

     // Getting distinct "to" and "from" requires a double unwind of arrays
     { "$unwind": "$to" },
     { "$unwind": "$to" },
     { "$unwind": "$from" },
     { "$unwind": "$from" },

     // And then adding back to sets for distinct
     { "$group": {
        "_id": "$_id",
        "total": { "$first": "$total" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { "$first": "$temp_hours" }
     }},

     // Map out for each hour and count size of distinct lists
     { "$project": {
        "count": "$total",
        "from_count": { "$size": "$from" },
        "to_count": { "$size": "$to" },
        "hours": {
            "$map": {
                "input": [
                     00,01,02,03,04,05,06,07,08,09,10,11,
                     12,13,14,15,16,17,18,19,20,21,22,23
                 ],
                 "as": "el",
                 "in": {
                      "$ifNull": [
                          { "$arrayElemAt": [
                              { "$map": {
                                  "input": { "$filter": {
                                     "input": "$temp_hours",
                                     "as": "tmp",
                                     "cond": {
                                         "$eq": [ "$$el", "$$tmp.index" ]
                                     }
                                  }},
                                 "as": "out",
                                 "in": "$$out.count"
                              }},
                              0
                          ]},
                          0
                      ]
                 }
            }
        }
     }},

     // Optionally sort in "uid" order
     { "$sort": { "_id": 1 } }
 ])

Prima di MongoDB 3.2 è necessario essere un po' più coinvolti per mappare il contenuto dell'array per tutte le ore del giorno:

db.collection.aggregate([

    // First group by hour within "uid" and keep distinct "to" and "from"
    { "$group": {
        "_id": {
            "uid": "$uid",
            "time": { "$hour": "$timestamp" }
        },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "count": { "$sum": 1 }
    }},

    // Roll-up to "uid" and keep each hour in an array
    { "$group": {
        "_id": "$_id.uid",
        "total": { "$sum": "$count" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { 
            "$push": {
                "index": "$_id.time",
                "count": "$count"
            }
        }
     }},

     // Getting distinct "to" and "from" requires a double unwind of arrays
     { "$unwind": "$to" },
     { "$unwind": "$to" },
     { "$unwind": "$from" },
     { "$unwind": "$from" },

     // And then adding back to sets for distinct, also adding the indexes array
     { "$group": {
        "_id": "$_id",
        "total": { "$first": "$total" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { "$first": "$temp_hours" },
        "indexes": { "$first": { "$literal": [
                     00,01,02,03,04,05,06,07,08,09,10,11,
                     12,13,14,15,16,17,18,19,20,21,22,23
        ] } }
     }},

     // Denormalize both arrays
     { "$unwind": "$temp_hours" },
     { "$unwind": "$indexes" },

     // Marry up the index entries and keep either the value or 0
     // Note you are normalizing the double unwind to distinct index
     { "$group": {
         "_id": {
             "_id": "$_id",
             "index": "$indexes"
         },
         "total": { "$first": "$total" }, 
         "from": { "$first": "$from" },
         "to": { "$first": "$to" },
         "count": {
             "$max": {
                 "$cond": [
                     { "$eq": [ "$indexes", "$temp_hours.index" ] },
                     "$temp_hours.count",
                     0
                 ]
             }
         }
     }},

     // Sort to keep index order - !!Important!!         
     { "$sort": { "_id": 1 } },

     // Put the hours into the array and get sizes for other results
     { "$group": {
         "_id": "$_id._id",
         "count": { "$first": "$total" },
         "from_count": { "$first": { "$size": "$from" } },
         "to_count": { "$first": { "$size": "$to" } },
         "hours": { "$push": "$count" }
     }},

     // Optionally sort in "uid" order
     { "$sort": { "_id": 1 } }
])

Per scomporlo, entrambi gli approcci qui seguono gli stessi passaggi di base, con l'unica vera differenza che si verifica sulla mappatura delle "ore" per il periodo di 24 ore.

Nella prima aggregazione $group fase, l'obiettivo è ottenere risultati per ora presenti nei dati e per ogni valore "uid". Il semplice operatore di aggregazione della data di $hour aiuta a ottenere questo valore come parte della chiave di raggruppamento.

Il $addToSet le operazioni sono di per sé una sorta di "mini-gruppo" e ciò consente di mantenere gli "insiemi distinti" per ciascuno dei valori "a" e "da" pur continuando essenzialmente a raggruppare per ora.

Il prossimo $group è più "organizzativo", poiché i "conteggi" registrati per ogni ora vengono mantenuti in un array mentre si arrotolano tutti i dati per essere semplicemente raggruppati per "uid". Questo fondamentalmente ti dà tutti i "dati" di cui hai veramente bisogno per il risultato, ma ovviamente il $addToSet le operazioni qui sono solo l'aggiunta di "array all'interno di array" degli insiemi distinti determinati all'ora.

Per ottenere questi valori come elenchi veramente distinti per ogni "uid" e solo, è necessario decostruire ogni array usando $unwind e poi, infine, raggruppare di nuovo solo come i distinti "set". Lo stesso $addToSet lo compatta e il $first le operazioni prendono solo i "primi" valori degli altri campi, che sono già tutti uguali per i dati di destinazione "per uid". Siamo contenti di quelli, quindi mantienili così come sono.

Le fasi finali qui sono essenzialmente di natura "cosmetica" e possono essere raggiunte ugualmente nel codice lato client. Poiché non sono presenti dati per ogni singolo intervallo di un'ora, è necessario mapparli in una matrice di valori che rappresentano ogni ora. I due approcci qui variano in base alle capacità degli operatori disponibili tra le versioni.

Nella versione MongoDB 3.2, ci sono $filter e $arrayElemAt operatori che consentono effettivamente di creare la logica per "trasporre" una sorgente di input di tutte le possibili posizioni di indice (24 ore) nei valori che sono già determinati per i conteggi di quelle ore nei dati disponibili. Questa è fondamentalmente una "ricerca diretta" di valori già registrati per ogni ora disponibile per vedere se esiste, dove fa il conteggio viene trasposto nell'intero array. Dove non è presente, un valore predefinito di 0 viene utilizzato sul posto.

Senza quegli operatori, fare questo "match up" significa essenzialmente denormalizzare entrambi gli array (i dati registrati e le 24 posizioni complete) per confrontare e trasporre. Questo è ciò che sta accadendo nel secondo approccio con un semplice confronto dei valori "indice" per vedere se c'è stato un risultato per quell'ora. Il $max operatore qui viene utilizzato principalmente a causa dei due $unwind dichiarazioni, in cui ogni valore registrato dai dati di origine verrà riprodotto per ogni possibile posizione dell'indice. Questo "comprime" solo i valori desiderati per "ora di indice".

In quest'ultimo approccio diventa quindi importante $sort nel gruppo _id valore. Questo perché contiene la posizione "indice" e sarà necessaria quando si sposta nuovamente questo contenuto in un array che si prevede venga ordinato. Che è ovviamente il $group finale fase qui in cui le posizioni ordinate vengono inserite in un array con $push .

Tornando alle "liste distinte", $size L'operatore viene utilizzato in tutti i casi per determinare la "lunghezza" e quindi il "conteggio" di valori distinti nelle liste per "a" e "da". Questo è l'unico vero vincolo almeno su MongoDB 2.6, ma può altrimenti essere sostituito semplicemente "svolgendo" ogni array individualmente e quindi raggruppando nuovamente su _id già presente per contare le voci dell'array in ogni set. È un processo di base, ma come dovresti vedere il $size operatore è l'opzione migliore qui per le prestazioni complessive.

Come nota finale, i tuoi dati di conclusione sono un po' fuori posto, poiché forse la voce con "ddd" in "from" doveva essere la stessa anche in "to", ma è invece registrata come "bbb". Questo cambia il conteggio distinto del terzo raggruppamento "uid" per "to" di una voce. Ma ovviamente i risultati logici dati i dati di origine sono validi:

{ "_id" : 1000000, "count" : 3, "from_count" : 2, "to_count" : 2, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0 ] }
{ "_id" : 2000000, "count" : 2, "from_count" : 1, "to_count" : 1, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0 ] }
{ "_id" : 3000000, "count" : 5, "from_count" : 5, "to_count" : 4, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0 ] }

NB La fonte ha anche un errore di battitura con il delimitatore interposto con : invece di una virgola subito dopo il timestamp su tutte le righe.