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

Raggruppa e conta utilizzando il framework di aggregazione

Sembra che tu abbia avuto un inizio su questo, ma ti sei perso su alcuni degli altri concetti. Ci sono alcune verità di base quando si lavora con gli array nei documenti, ma iniziamo da dove eri rimasto:

db.sample.aggregate([
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 }
    }}
])

Quindi utilizzerà il $group pipeline per raccogliere i tuoi documenti sui diversi valori del campo "status" e quindi produrre anche un altro campo per "count" che ovviamente "conta" le occorrenze della chiave di raggruppamento passando un valore di 1 al $sum operatore per ogni documento trovato. Questo ti mette a un punto molto simile a quello che descrivi:

{ "_id" : "done", "count" : 2 }
{ "_id" : "canceled", "count" : 1 }

Questa è la prima fase e abbastanza facile da capire, ma ora devi sapere come ottenere valori da un array. Potresti essere tentato una volta che avrai compreso la "dot notation" concetto correttamente per fare qualcosa del genere:

db.sample.aggregate([
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$devices.cost" }
    }}
])

Ma quello che scoprirai è che il "totale" sarà infatti 0 per ciascuno di questi risultati:

{ "_id" : "done", "count" : 2, "total" : 0 }
{ "_id" : "canceled", "count" : 1, "total" : 0 }

Come mai? Bene, operazioni di aggregazione di MongoDB come questa non attraversano effettivamente gli elementi dell'array durante il raggruppamento. Per fare ciò, il framework di aggregazione ha un concetto chiamato $unwind . Il nome è relativamente autoesplicativo. Un array incorporato in MongoDB è molto simile ad avere un'associazione "uno-a-molti" tra origini dati collegate. Allora cosa $unwind fa è esattamente quel tipo di risultato "unita", in cui i "documenti" risultanti si basano sul contenuto dell'array e sulle informazioni duplicate per ciascun genitore.

Quindi, per agire sugli elementi dell'array è necessario utilizzare $unwind primo. Questo dovrebbe logicamente portarti a codificare in questo modo:

db.sample.aggregate([
    { "$unwind": "$devices" },
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$devices.cost" }
    }}
])

E poi il risultato:

{ "_id" : "done", "count" : 4, "total" : 700 }
{ "_id" : "canceled", "count" : 2, "total" : 350 }

Ma non è del tutto corretto, vero? Ricorda cosa hai appena imparato da $unwind e come fa un join denormalizzato con le informazioni del genitore? Quindi ora è duplicato per ogni documento poiché entrambi avevano due membri dell'array. Quindi, mentre il campo "totale" è corretto, il "conteggio" è il doppio di quanto dovrebbe essere in ogni caso.

È necessario prestare un po' più di attenzione, quindi invece di farlo in un unico $group fase, si fa in due:

db.sample.aggregate([
    { "$unwind": "$devices" },
    { "$group": {
        "_id": "$_id",
        "status": { "$first": "$status" },
        "total": { "$sum": "$devices.cost" }
    }},
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$total" }
    }}
])

Che ora ottiene il risultato con i totali corretti:

{ "_id" : "canceled", "count" : 1, "total" : 350 }
{ "_id" : "done", "count" : 2, "total" : 700 }

Ora i numeri sono giusti, ma non è ancora esattamente quello che chiedi. Penso che dovresti fermarti qui poiché il tipo di risultato che ti aspetti non è davvero adatto a un singolo risultato dalla sola aggregazione. Stai cercando che il totale sia "dentro" il risultato. Non è proprio lì, ma con dati piccoli va bene:

db.sample.aggregate([
    { "$unwind": "$devices" },
    { "$group": {
        "_id": "$_id",
        "status": { "$first": "$status" },
        "total": { "$sum": "$devices.cost" }
    }},
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$total" }
    }},
    { "$group": {
        "_id": null,
        "data": { "$push": { "count": "$count", "total": "$total" } },
        "totalCost": { "$sum": "$total" }
    }}
])

E un modulo del risultato finale:

{
    "_id" : null,
    "data" : [
            {
                    "count" : 1,
                    "total" : 350
            },
            {
                    "count" : 2,
                    "total" : 700
            }
    ],
    "totalCost" : 1050
}

Ma "Non farlo" . MongoDB ha un limite di documenti sulla risposta di 16 MB, che è una limitazione delle specifiche BSON. Su piccoli risultati puoi eseguire questo tipo di comodo wrapping, ma nello schema più ampio di cose vuoi i risultati nel modulo precedente e una query separata o vivere con l'iterazione dell'intero risultato per ottenere il totale da tutti i documenti.

Sembra che tu stia utilizzando una versione MongoDB inferiore alla 2.6 o che copi l'output da una shell RoboMongo che non supporta le funzionalità della versione più recente. Da MongoDB 2.6, tuttavia, i risultati dell'aggregazione possono essere un "cursore" anziché un singolo array BSON. Quindi la risposta complessiva può essere molto più grande di 16 MB, ma solo quando non stai compattando in un singolo documento come risultati, come mostrato nell'ultimo esempio.

Ciò sarebbe particolarmente vero nei casi in cui stavi "impaginando" i risultati, con da 100 a 1000 di righe di risultati ma volevi solo un "totale" da restituire in una risposta API quando stai restituendo solo una "pagina" di 25 risultati in una volta.

In ogni caso, questo dovrebbe darti una guida ragionevole su come ottenere il tipo di risultati che ti aspetti dal tuo modulo di documento comune. Ricorda $unwind per elaborare gli array e in generale $group più volte per ottenere totali a diversi livelli di raggruppamento dal documento e dai raggruppamenti di raccolta.