MongoDB et permet realitzar diverses operacions d'agregació que et permeten processar els registres de dades de diverses maneres, com ara agrupar dades, ordenar les dades en un ordre específic o reestructurar els documents retornats, així com filtrar les dades com es podria fer amb una consulta.
Introducció
MongoDB ofereix operacions d'agregació mitjançant "pipelines" d'agregació : una sèrie d'operacions que processen documents de dades de manera seqüencial.
Les "pipelines" d'agregació es construeixen com una sèrie seqüencial d'operacions declaratives de processament de dades conegudes com a etapes . Cada etapa inspecciona i transforma els documents a mesura que passen per la pipeline (canonada), alimentant els resultats transformats a les etapes posteriors per a un processament posterior.
Els documents d'una col·lecció escollida entren al pipeline i passen per cada etapa, on la sortida procedent d'una etapa constitueix l'entrada per a la següent i el resultat final arriba al final del pipeline.
flowchart LR d1@{ shape: paper-tape, label: "Documents"} d2@{ shape: paper-tape, label: "Documents"} s1([Filtrar]) s2([Transformar]) s3([Ordenar]) d1 --> s1 s1 ==> s2 s2 ==> s3 s3 --> d2 style s1 fill:#00f style s2 fill:#080 style s3 fill:#800
Les etapes poden realitzar operacions sobre dades com ara:
- Filtrar. S'assembla a les consultes, on la llista de documents es redueix a través d'un conjunt de criteris
- Ordenar. Pots reordenar els documents en funció d'un camp escollit
- Transformar. La possibilitat de canviar l'estructura dels documents significa que podeu eliminar o canviar el nom de determinats camps, o potser canviar el nom o agrupar camps dins d'un document incrustat per a la seva llegibilitat.
- Agrupar. També pots processar diversos documents junts per formar un resultat resumit.
Les etapes de pipeline no necessiten produir el mateix nombre de documents que reben.
Agregació
classDiagram direction LR class City { _id: ObjectId name: string country: string continent: string population: number }
A continuació inserta un conjunt de dades a la col.lecció cities
:
test> db.cities.drop()
test > db.cities.insertMany([
{"name": "Seoul", "country": "South Korea", "continent": "Asia", "population": 25.674 },
{"name": "Mumbai", "country": "India", "continent": "Asia", "population": 19.980 },
{"name": "Lagos", "country": "Nigeria", "continent": "Africa", "population": 13.463 },
{"name": "Beijing", "country": "China", "continent": "Asia", "population": 19.618 },
{"name": "Shanghai", "country": "China", "continent": "Asia", "population": 25.582 },
{"name": "Osaka", "country": "Japan", "continent": "Asia", "population": 19.281 },
{"name": "Cairo", "country": "Egypt", "continent": "Africa", "population": 20.076 },
{"name": "Tokyo", "country": "Japan", "continent": "Asia", "population": 37.400 },
{"name": "Karachi", "country": "Pakistan", "continent": "Asia", "population": 15.400 },
{"name": "Dhaka", "country": "Bangladesh", "continent": "Asia", "population": 19.578 },
{"name": "Rio de Janeiro", "country": "Brazil", "continent": "South America", "population": 13.293 },
{"name": "São Paulo", "country": "Brazil", "continent": "South America", "population": 21.650 },
{"name": "Mexico City", "country": "Mexico", "continent": "North America", "population": 21.581 },
{"name": "Delhi", "country": "India", "continent": "Asia", "population": 28.514 },
{"name": "Buenos Aires", "country": "Argentina", "continent": "South America", "population": 14.967 },
{"name": "Kolkata", "country": "India", "continent": "Asia", "population": 14.681 },
{"name": "New York", "country": "United States", "continent": "North America", "population": 18.819 },
{"name": "Manila", "country": "Philippines", "continent": "Asia", "population": 13.482 },
{"name": "Chongqing", "country": "China", "continent": "Asia", "population": 14.838 },
{"name": "Istanbul", "country": "Turkey", "continent": "Europe", "population": 14.751 }
])
Important!. La població està expressada en milions d'habitants (en número decimal) 🫡
$match
Per crear un pipeline d'agregació pots utilitzar el mètode aggregate()
que utilitza una sintaxi que és força semblant al mètode find()
que s'utilitza per consultar dades d'una col·lecció.
$match
s'utilitza per reduir la llista de documents en qualsevol pas donat d'una "pipeline" i es pot utilitzar per assegurar que totes les operacions posteriors s'executen en una llista limitada d'entrades.
L'operació que es mostra a continuació construeix una pipeline d'agregació utilitzant una sola etapa $match
sense cap consulta de filtratge en particular:
test> db.cities.aggregate([
... { $match: {} }
... ])
[
{
_id: ObjectId('678a9770522f6dfeb6cb0ce2'),
name: 'Seoul',
country: 'South Korea',
continent: 'Asia',
population: 25.674
},
...
Com que les "pipelines" d'agregació són processos de diversos passos, l'argument és una llista d'etapes, d'aquí l'ús de claudàtors []
que denoten una llista d'elements múltiples, on cada element dins d'aquesta llista és un objecte que descriu una etapa de processament.
A continuació, torna a executar el mètode aggregate()
incloent un document de consulta com a paràmetre de l'etapa $match
.
test> db.cities.aggregate([ { $match: { continent: "Europe"} }] )
[
{
_id: ObjectId('678a9770522f6dfeb6cb0cf5'),
name: 'Istanbul',
country: 'Turkey',
continent: 'Europe',
population: 14.751
}
]
Aquesta ordre retorna els mateixos documents que el mètode find()
tal com pots comprovar a continuació:
test> db.cities.find({ continent: "Europe"})
[
{
_id: ObjectId('678a9770522f6dfeb6cb0cf5'),
name: 'Istanbul',
country: 'Turkey',
continent: 'Europe',
population: 14.751
}
]
A continuació modifica la etapa $match
perquè torni ciutats d'Europa i d'Amèrica del Nord:
> db.cities.aggregate([ { $match: { continent: { $in: [ "Europe", "North America"] }}}])
[
{
_id: ObjectId('678a9770522f6dfeb6cb0cee'),
name: 'Mexico City',
country: 'Mexico',
continent: 'North America',
population: 21.581
},
...
Pots veure que la sintaxi del document de consulta torna a ser idèntica a com recuperares les mateixes dades amb el mètode find()
.
$sort
L'etapa $match
és útil per reduir la llista de documents que es traslladen a la següent fase d'agregació, però no fa res per canviar o transformar les dades a mesura que passen pel pipeline.
Mitjançant el mecanisme de consulta estàndard, pots especificar l'ordre del documents afegint un mètode sort()
al final d'una consulta find()
.
Per exemple, pots recuperar totes les ciutats de la col·lecció i ordenar-les en ordre descendent per població:
test> db.cities.find().sort({"population": -1})
[
{
_id: ObjectId('678a9770522f6dfeb6cb0ce9'),
name: 'Tokyo',
country: 'Japan',
continent: 'Asia',
population: 37.4
},
...
També pots ordenar els documents en una pipeline d'agregació incloent una etapa $sort
:
test> db.cities.aggregate([
... { $sort: { "population": -1 }}
... ])
[
{
_id: ObjectId('678a9770522f6dfeb6cb0ce9'),
name: 'Tokyo',
country: 'Japan',
continent: 'Asia',
population: 37.4
},
...
MongoDB retorna el mateix conjunt de resultats que l'operació find()
d'abans, ja que utilitzar un pipeline d'agregació amb només una fase d'ordenació és equivalent a una consulta estàndard amb un ordre d'ordenació aplicat.
A continuació recupera només les ciutats d'Amèrica del Nord ordenades per població en ordre ascendent.
Per fer-ho, pots aplicar dues etapes de processament una després de l'altra:
- La primera per reduir el conjunt de resultats amb una etapa
$match
de filtratge - Després una segona per aplicar l'ordenació requerida mitjançant una etapa
$sort
.
flowchart LR d1@{ shape: paper-tape, label: "Cities"} d2@{ shape: paper-tape, label: "Cities"} s1([$match]) s3([$sort]) d1 --> s1 s1 ==> s3 s3 --> d2 style s1 fill:#00f style s3 fill:#800
test> db.cities.aggregate([
... { $match: {"continent": "North America"} },
... { $sort: {"population": -1} },
... ])
[
{
_id: ObjectId('678a9770522f6dfeb6cb0cee'),
name: 'Mexico City',
country: 'Mexico',
continent: 'North America',
population: 21.581
},
...
$group
L'etapa $group
s'encarrega d'agrupar i resumir els documents.
Admet diversos documents i els organitza en diversos lots separats basant-se en l'agrupació de valors d'expressió i produeix un únic document per a cada lot diferent.
Els documents de sortida contenen informació sobre el grup i poden contenir camps calculats addicionals com sumes o mitjanes a la llista de documents del grup.
Per exemple, pots agrupar el documents pel continent en què es troba cada ciutat:
test> db.cities.aggregate([
{ $group: { "_id": "$continent" } }
)]
[
{ _id: 'Asia' },
{ _id: 'South America' },
{ _id: 'Europe' },
{ _id: 'Africa' },
{ _id: 'North America' }
]
Quan fas un "insert" el camp _id
es genera automàticament si no n'especifiques un de manera determinada, perquè a MongoDB cada document ha de tenir un camp _id
per utilitzar-lo com a clau primària.
En canvi, en una etapa $group
cal que especifiquis un camp _id
amb una expressió vàlida.
Cada vegada que fas referència als valors d'un camp en una "pipeline" d'agregació has de precedir el nom del camp amb un signe de dòlar ($)
.
A MongoDB, això es coneix com a "field path", ja que dirigeix l'operació al camp adequat on pot trobar els valors que s'utilitzaran en l'etapa de pipeline.
També pots especificar diversos valors d'un sol camp en una expressió d'agrupació.
A continuació agrupem els documents en funció dels valors continent
i country
:
test> db.cities.aggregate([
{ $group: { "_id": {"continent": "$continent", "country": "$country" }} }
])
[
{ _id: { continent: 'North America', country: 'United States' } },
{ _id: { continent: 'Asia', country: 'Japan' } },
{ _id: { continent: 'Africa', country: 'Nigeria' } },
{ _id: { continent: 'Asia', country: 'Bangladesh' } },
{ _id: { continent: 'Asia', country: 'Pakistan' } },
{ _id: { continent: 'Africa', country: 'Egypt' } },
{ _id: { continent: 'Asia', country: 'China' } },
{ _id: { continent: 'South America', country: 'Brazil' } },
{ _id: { continent: 'Asia', country: 'South Korea' } },
{ _id: { continent: 'North America', country: 'Mexico' } },
{ _id: { continent: 'South America', country: 'Argentina' } },
{ _id: { continent: 'Asia', country: 'India' } },
{ _id: { continent: 'Asia', country: 'Philippines' } },
{ _id: { continent: 'Europe', country: 'Turkey' } }
]
En aquest cas el camp _id
de l'expressió d'agrupació utilitza un document incrustat que, al seu torn, té dos camps dins: un per al nom del continent i un altre per al nom del país.
Per realitzar anàlisis de dades més complexes MongoDB ofereix una sèrie d'operadors d'acumuladors que et permeten trobar detalls més granulars sobre les teves dades.
Un operador d'acumulador, de vegades anomenat simplement acumulador, és un tipus especial d'operació que manté el seu valor o estat mentre passa per una "pipeline", com ara una suma o una mitjana de més d'un valor.
Per exemple, si vols saber quina és el valor màxim de població d'una ciutat en cada continent, pots utilitzar l'acumulador $max
:
test> db.cities.aggregate([
{ $group: { "_id": "$continent", "highest_population": { $max: "$population" }} }
])
[
{ _id: 'Asia', highest_population: 37.4 },
{ _id: 'South America', highest_population: 21.65 },
{ _id: 'Europe', highest_population: 14.751 },
{ _id: 'Africa', highest_population: 20.076 },
{ _id: 'North America', highest_population: 21.581 }
]
Amb l'operador $sum
, torna el total de la població de cada continent:
test> db.cities.aggregate([
{ $group: { "_id": "$continent", "population": { $sum: "$population" }}
}])
[
{ _id: 'Europe', population: 14.751 },
{ _id: 'South America', population: 49.91 },
{ _id: 'North America', population: 40.4 },
{ _id: 'Africa', population: 33.539 },
{ _id: 'Asia', population: 254.028 }
]
En aquest enllaç tens la llista completa dels operadors d'agregació que pots utilitzar: Operadors d'agregació
$project
Amb $project
pots tornar només alguns dels múltiples camps d'una col·lecció de documents o canviar una mica l'estructura per moure alguns camps als documents incrustats.
Imagina't que només vols recuperar el nom i la població de cadascuna de les ciutats:
Executa el mètode aggregate()
amb una etapa $project
:
test> db.cities.aggregate([ { $project: {
"_id": 0,
"name": 1,
"population": "$population"
}}])
[
{ name: 'Seoul', population: 25.674 },
{ name: 'Mumbai', population: 19.98 },
...
Amb els valors 0
i 1
pots excloure o incloure un camp a la projecció.
Per defecte, el valor del camp _id
s'inclou als documents de sortida.
Operadors d'agregació
$sum
https://www.mongodb.com/docs/manual/reference/operator/aggregation/sum/
Activitats
1.- Troba la ciutat més poblada per a cada país d'Àsia i Amèrica del Nord i retornar el nom i la població.
test> db.cities.aggregate([
{ $match: { "continent": { $in: ["Asia", "North America"]}}},
{ $sort: { "population": -1}},
{ $group: { "_id": "$continent", "name": { "$first": "$name"}, "population": { "$first": "$population"} }},
])
2.- Inserta aquestes dades:
classDiagram direction LR class Pizza { _id: ObjectId name: string size: string price: number date: ISODate }
test> db.orders.insertMany( [
{ _id: 0, name: "Pepperoni", size: "small", price: 19, quantity: 10, date: ISODate( "2021-03-13T08:14:30Z" ) },
{ _id: 1, name: "Pepperoni", size: "medium", price: 20, quantity: 20, date : ISODate( "2021-03-13T09:13:24Z" ) },
{ _id: 2, name: "Pepperoni", size: "large", price: 21, quantity: 30, date : ISODate( "2021-03-17T09:22:12Z" ) },
{ _id: 3, name: "Cheese", size: "small", price: 12, quantity: 15, date : ISODate( "2021-03-13T11:21:39.736Z" ) },
{ _id: 4, name: "Cheese", size: "medium", price: 13, quantity:50, date : ISODate( "2022-01-12T21:23:13.331Z" ) },
{ _id: 5, name: "Cheese", size: "large", price: 14, quantity: 10, date : ISODate( "2022-01-12T05:08:13Z" ) },
{ _id: 6, name: "Vegan", size: "small", price: 17, quantity: 10, date : ISODate( "2021-01-13T05:08:13Z" ) },
{ _id: 7, name: "Vegan", size: "medium", price: 18, quantity: 10, date : ISODate( "2021-01-13T05:10:13Z" ) }
] )
Retorna la quantitat total de comanda de pizzes de mida mitjana ("medium") agrupades pel nom de la pizza:
test> db.orders.aggregate( [
{ $match: { size: "medium" } },
{ $group: { _id: "$name", totalQuantity: { $sum: "$quantity" } } }
] )
[
{ _id: 'Pepperoni', total: 20 },
{ _id: 'Cheese', total: 50 },
{ _id: 'Vegan', total: 10 }
]
Zips
Aquestes activitats utilitzen la col·lecció zipcodes
.
Cada document de la col·lecció zipcodes
té aquesta forma:
{
"_id": "10280",
"city": "NEW YORK",
"state": "NY",
"pop": 5574,
"loc": [ -74.016323, 40.710537 ]
}
Una ciutat pot tenir més d'un codi postal associat, ja que diferents parts de la ciutat poden tenir un codi postal diferent.
Importa la col.lecció zipcodes
:
> curl https://gitlab.com/xtec/data/mongo-data/-/raw/main/zipcodes.json -o zipcodes.json
> mongoimport.exe .\zipcodess.json
1.- Retorna tots els estats amb una població total superior a 10 milions:
test> db.zipcodes.aggregate([
{ $group: { "_id": "$state", "pop": { $sum: "$pop"}}},
{ $match: { "pop": { $gt: 10000000}}},
{ $sort: {"pop": -1}}
])
[
{ _id: 'CA', pop: 29754890 },
{ _id: 'NY', pop: 17990402 },
{ _id: 'TX', pop: 16984601 },
{ _id: 'FL', pop: 12686644 },
{ _id: 'PA', pop: 11881643 },
{ _id: 'IL', pop: 11427576 },
{ _id: 'OH', pop: 10846517 }
]
La consulta SQL equivalent és:
SELECT state, SUM(pop)
FROM zipcodes
GROUP BY state
HAVING totalPop >= 10000000
2.- Retorna la població mitjana de les ciutats de cada estat:
test> db.zipcodes.aggregate([
{ $group: { "_id": { "state": "$state", "city": "$city" }, "pop": { $sum: "$pop"}}},
{ $group: { "_id": "$_id.state", "pop": { $avg: "$pop"}}},
{ $sort: {"pop": -1}}
])
[
{ _id: 'DC', pop: 303450 },
{ _id: 'CA', pop: 27756.42723880597 },
...
3.- Retorna les ciutats més grans per població per a cada estat:
db.zipcodes.aggregate([
{ $group: { "_id": { "state": "$state", "city": "$city" }, "pop": { $sum: "$pop"}}},
{ $sort: {"pop": -1}},
{ $group: { "_id": "$_id.state", "city": {$first: "$_id.city"}, "pop": {$first: "$pop"}}},
{ $sort: {"pop": -1}},
])
[
{ _id: 'IL', city: 'CHICAGO', pop: 2452177 },
{ _id: 'NY', city: 'BROOKLYN', pop: 2300504 },
{ _id: 'CA', city: 'LOS ANGELES', pop: 2102295 },
{ _id: 'TX', city: 'HOUSTON', pop: 2095918 },
...
Preferències d'usuari
A continuació treballaras amb una col·lecció members
d'un club esportiu que fa un seguiment dels noms dels seus membres, les dates de quan es van unir i les preferències esportives:
test > db.members.insertMany( [
{
_id: "jane",
joined: ISODate("2011-03-02"),
likes: ["golf", "racquetball"]
},
{
_id: "joe",
joined: ISODate("2012-07-02"),
likes: ["tennis", "golf", "swimming"]
},
{
_id: "ruth",
joined: ISODate("2012-01-14"),
likes: ["golf", "racquetball"]
},
{
_id: "harold",
joined: ISODate("2012-01-21"),
likes: ["handball", "golf", "racquetball"]
},
{
_id: "kate",
joined: ISODate("2012-01-14"),
likes: ["swimming", "tennis"]
}
] )
1.- Retorna els noms dels membres en majúscules i per ordre alfabètic.
test> db.members.aggregate([
{ $project: { "_id":0, "name": { $toUpper: "$_id"} }},
{ $sort: { "name": 1}}
])
[
{ name: 'harold' },
{ name: 'jane' },
{ name: 'joe' },
{ name: 'kate' },
{ name: 'ruth' }
]
2.- Torna els noms d'usuari ordenats pel mes que es van unir al club:
test> db.members.aggregate([
{ $sort: { "joined": 1 } },
{ $project: {"_id":0, "name": "$_id", "joined": {$dateToString: { format: "%Y-%m-%d", date: "$joined" }}}}
])
[
{ name: 'jane', joined: '2011-03-02' },
{ name: 'ruth', joined: '2012-01-14' },
{ name: 'kate', joined: '2012-01-14' },
{ name: 'harold', joined: '2012-01-21' },
{ name: 'joe', joined: '2012-07-02' }
]
3.- Retorna el nombre total d'unions per mes:
test> db.members.aggregate([
{ $project: { "month": { $month: "$joined" }}},
{ $group: { "_id": "$month", "number": { $sum: 1 }}},
{ $sort: { "_id": 1}},
{ $project: { "_id":0 , "month": "$_id", "number": 1 }} ])
[
{ number: 3, month: 1 },
{ number: 1, month: 3 },
{ number: 1, month: 7 }
]
4.- Torna les tres activitats amb més "m'agrada" del conjunt de dades.
Ajuda L'operador $unwind
separa cada valor de la llista likes
i crea una nova versió del document original per a cada element de la llista.
test> db.members.aggregate([
{ $unwind: "$likes" },
{ $group: {"_id": "$likes", "number": { $sum: 1}}},
{ $sort: { "number": -1 }},
{ $limit: 3}
])
[
{ _id: 'golf', number: 4 },
{ _id: 'racquetball', number: 3 },
{ _id: 'tennis', number: 2 }
]
Recursos
En aquest enllaç tens un llibre gratuit: Practical MongoDB Aggregations