Quando si ha a che fare con una gran mole di dati – e MongoDB è nato proprio per questo scopo – una delle funzionalità più importanti è l’aggregazione, che permette di avere una visione globale dei dati e di ricavarne statistiche. L’aggregazione è oggi una funzionalità essenziale in ambiti come Business Intelligence e Big Data.
MongoDB supporta due meccanismi per effettuare aggregazioni. Uno è quello storicamente più noto in NoSQL: MapReduce. Di recente, però, è stato introdotto anche l’Aggregation Framework. Quest’ultimo è il meccanismo raccomandato, sia per la sua semplicità ma anche per ragioni di performance. Lo vedremo per primo.
Aggregation Framework
Partiamo subito da un esempio. Apriamo la shell Mongo e simuliamo un po’ di dati casuali, immaginando un database contenente i log di accesso ad un sito, allo stesso modo con cui l’abbiamo fatto nelle precedenti lezioni.
> for(var i=1; i<=20000; i++) {
db.logs.insert( { tipo: "Login", gruppo: "Administrators", data: new Date() } );
db.logs.insert( { tipo: "Login", gruppo: "Users", data: new Date() } );
}
> for(var i=1; i<=20000; i++) {
db.logs.insert( { tipo: "Click", gruppo: "Users", page: "home.html", data: new Date(), operazioni: [5,2,4] } );
db.logs.insert( { tipo: "Click", gruppo: "Users", page: "mongodb.html", data: new Date(), operazioni: [3,2,1] } );
}
A questo punto, se siamo interessati a sapere quanti eventi di tipo Login ci sono stati nella nostra applicazione, raggruppati per gruppo di utente, possiamo lanciare il seguente comando:
> db.logs.aggregate( [
{ $match: { tipo: "Login" } },
{ $group: { _id: "$gruppo", conteggio: { $sum: 1 } } },
])
Notiamo subito alcune cose: il comando usato è aggregate
. Questo comando accetta come parametro una pipeline di operatori, cioè un elenco ordinato di operatori di aggregazione. L’effetto è che la collezione dei documenti (in questo caso logs
) passa tra i vari stadi della pipeline. Per avere le migliori performance è quindi importante mettere all’inizio della pipeline le operazioni di filtraggio in modo da far passare meno elementi possibili attraverso il resto della pipeline.
In questo esempio abbiamo:
-
match
: effettua un filtraggio dei documenti. La sintassi è la stessa che abbiamo visto nel filtro difind
-
group
: effettua un raggruppamento dei dati. Il risultato del raggruppamento è espresso nell’oggetto BSON che segue. In particolare vediamo che come_id
conteggio
Accodando vari operatori di aggregazione si possono effettuare molte operazioni. Per un elenco esaustivo rimandiamo alla guida ufficiale. Alcuni operatori utili sono:
-
limit
$limit: 100
-
skip
$skip: 10
- project
-
sort
$sort: { conteggio: 1 }
-
unwind
: letteralmente “srotola” un documento contenente un array di N elementi, producendo un elenco di N documenti, ciascuno contenente un solo elemento.
Vediamo un esempio:
> db.logs.aggregate( [
{ $project: { tipo: 1, operazioni: 1 } },
{ $unwind: "$operazioni" },
{ $group: { _id: "$tipo", sommaOperazioni: { $sum: "$operazioni" } } }
])
Dopo aver considerato solo i campi tipo
operazioni
operazioni
{ tipo: "Click", operazioni: [5,2,4] }
viene trasformato nei documenti:
{ tipo: "Click", operazioni: 5 }
{ tipo: "Click", operazioni: 2 }
{ tipo: "Click", operazioni: 4 }
Infine l’ultimo passo della pipeline effettua un raggruppamento con somma del valore di operazioni
MapReduce
Come anticipato, attualmente Aggregation Framework è il framework raccomandato per effettuare aggregazioni sui dati in MongoDB. La sua semplicità è data dalla sintassi dichiarativa, mentre le migliori performance sono dovute al fatto che le operazioni possibili sono già codificate. MapReduce si basa invece fondamentalmente su un paradigma più imperativo e sulla flessibilità data dal JavaScript per dichiarare le operazioni da svolgere. Il vantaggio di MapReduce è che è più flessibile e che non tutte le operazioni che si possono implementare con MapReduce sono supportate dall’Aggregation Framework. Per cui la raccomandazione, quando si devono aggregare dati, è di utilizzare Aggregation Framework se esso permette di effettuare le operazioni richieste, altrimenti optare per con MapReduce.
MapReduce richiede che gli si forniscano due funzioni: map
lavora su un singolo documento, mentre la funzione reduce
opera sull’elenco risultante con lo scopo di realizzare l’aggregazione effettiva. Ad esempio se avessimo voluto implementare un calcolo analogo all’ultimo visto con Aggregation Framework, avremmo dovuto scrivere:
> db.logs.mapReduce(
function() { if(this.operazioni) {
var i; for(i=0; i<this.operazioni.length; i++)
emit(this.tipo, this.operazioni[i] );
}},
function(key,values) { return Array.sum(values); },
{ out: {inline:1} }
)
Vediamo come nella funzione map
emit
per ogni elemento nell’array operazioni
emit
reduce
chiave,valore
reduce
key,value
emit
out
La funzione MapReduce ha molti altri parametri e offre ulteriori funzionalità. Dal momento che non la approfondiremo ulteriormente, rimandiamo alla guida ufficiale.
Ti consigliamo anche