Diferència entre revisions de la pàgina «MongoDB»

De Cacauet Wiki
Salta a la navegació Salta a la cerca
 
(Hi ha 104 revisions intermèdies del mateix usuari que no es mostren)
Línia 1: Línia 1:
 +
MongoDB és una base de dades no-SQL, orientada a documents i principalment utilitzada per aplicacions web, buscant rendiments alts i escalables.
 +
 +
Altres articles relacionats:
 +
* Python per MongoDB: [http://api.mongodb.org/python/current/tutorial.html llibreria Pymongo]
 +
* [[MongoDB: escalant]]
 +
<br>
 +
 
== Intro No-SQL ==
 
== Intro No-SQL ==
 
Algunes característiques:
 
Algunes característiques:
* Treballem amb documents.
+
* Orientada a documents.
 
* La inconsistència de dades deixa de ser una prioritat.
 
* La inconsistència de dades deixa de ser una prioritat.
 
* Treballem amb incrustació (embedding). Exemple de blog: posts+comments
 
* Treballem amb incrustació (embedding). Exemple de blog: posts+comments
Línia 16: Línia 23:
 
**: Hi ha una variant típica de 1:pocs que és el cas que ens agrada per incrustar.
 
**: Hi ha una variant típica de 1:pocs que és el cas que ens agrada per incrustar.
 
** N:N (molts:molts) calen llistes (es poden posar a les 2 col·leccions tot i que això significa que la app ha de controlar la consistència
 
** N:N (molts:molts) calen llistes (es poden posar a les 2 col·leccions tot i que això significa que la app ha de controlar la consistència
* Sempre intentem incrustar docs.
+
* Sempre intentem incrustar docs = PRE-JOINED data
  
 
Info:
 
Info:
Línia 24: Línia 31:
 
* memcache is not highly functional but scales well
 
* memcache is not highly functional but scales well
 
* Joins don't scale well
 
* Joins don't scale well
 +
* Work with pre-joined data
 
* Documents have a 16 MB limit on 32 bit OS's
 
* Documents have a 16 MB limit on 32 bit OS's
 
* Transactions are unavailable
 
* Transactions are unavailable
Línia 29: Línia 37:
 
* 'id' is immutable, and made up of Current Time, Machine Id, Process ID and global Counter
 
* 'id' is immutable, and made up of Current Time, Machine Id, Process ID and global Counter
 
* remove is not an isolated transaction.
 
* remove is not an isolated transaction.
 +
 +
 +
Referències:
 +
* http://www.mongodb.org/
 +
* Comparació SQL <-> MongoDB : http://docs.mongodb.org/manual/reference/sql-comparison/
 +
* JavaScript + Node.js : http://mongoosejs.com/
 +
* ...
  
 
<br>
 
<br>
Línia 39: Línia 54:
 
* Seleccionar una db (o crear):<pre>> use <nom_db></pre>
 
* Seleccionar una db (o crear):<pre>> use <nom_db></pre>
  
 +
* Per saber les bases de dades que tenim:<pre>> show dbs</pre>
 +
* Un cop hem entrat en una DB amb "use", per saber les col·leccions: <pre>> show collections</pre>
 +
 +
* Exemple amb dades:
 +
** [[Fitxer:Notes.js]]
 +
** [[Fitxer:Alumnes.js]]
 +
* Importar:<pre>$ mongoimport -d <base_de_dades> -c <col·leccio> < <arxiu_json></pre>
 +
* En aquest cas:<pre>$ mongoimport -d students -c grades < grades.ef42a2b3e7ff.js</pre>
 +
* Comparació SQL <-> MongoDB : http://docs.mongodb.org/manual/reference/sql-comparison/
 +
* ...
  
*Exemple amb dades:
 
*: https://education.mongodb.com/static/10gen_2013_M101P_November/handouts/grades.ef42a2b3e7ff.js
 
* Importar-lo:<pre>$ mongoimport -d students -c grades < grades.ef42a2b3e7ff.js</pre>
 
*...
 
  
 
=== JavaScript en la Mongo Shell ===
 
=== JavaScript en la Mongo Shell ===
Línia 49: Línia 70:
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
 
for (i=0;i<100000;i++) {
 
for (i=0;i<100000;i++) {
   db.coses.save( {
+
   coses = ["tablet","smartphone","xocolata"]
       "num":i,
+
  var elem = {
       "cosa": ["tablet","smartphone","xocolata"][Math.floor(Math.random()*3)]
+
       "num": i,
   })
+
       "cosa": coses[Math.floor(Math.random()*3)]
 +
   }
 +
  db.coses.save( elem )
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Un altre exemple:
+
Un altre exemple amb subdocuments:
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
 
for (i=0;i<1000;i++) {
 
for (i=0;i<1000;i++) {
Línia 69: Línia 92:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
Si volem utilitzar els resultats d'una query en un script per mostrar dades o fer subqueries:
 +
<syntaxhighlight lang="javascript">
 +
var img = db.images.findOne( {_id:33} );
 +
// findOne ens torna un diccionari (array associatiu) JSON. Podem accedir als elements amb:
 +
print( img["width"] );
 +
print( img.width );
 +
print( img );
 +
</syntaxhighlight>
 +
 +
ULL: això ens funciona bé per findOne() i per aggregate(), <strike>no per find()</strike>.
 +
 +
=== Cursors ===
 +
Un .findOne() o un .aggregate() ens torna un objecte JSON amb la resposta demanada (veure més avall la sintaxi d'ambdós).
 +
 +
En canvi, un find() ens torna un cursor, amb el què hem d'acabar de realitzar l'extracció de les dades una a una, en un bucle:
 +
 +
<syntaxhighlight lang="javascript">
 +
var imgCursor = db.images.find( {"tags":"cats"} );
 +
// per mostrar el 4rt element:
 +
elem = imgCursor[4];
 +
// Per obtenir el següent element:
 +
var image = imgCursor.next();
 +
// Per iterar per cadscun d'ells:
 +
while( imgCursor.hasNext() ) {
 +
    var image = imgCursor.next();
 +
    print(image._id);
 +
    // o bé:
 +
    print(image["width"]);
 +
    // per mostrar tot l'objecte (no sé perquè només surt 1. (TODO: arreglar-ho...)
 +
    JSON.stringify( image );
 +
    // o, només per mongoShell, directament:
 +
    image;
 +
}
 +
</syntaxhighlight>
 +
 +
Altres maneres d'iterar una col·lecció (foreach síncron):
 +
<syntaxhighlight lang="javascript">
 +
for( var iter=db.cotxes.find(); iter.hasNext();  ) {
 +
    var cotxe = iter.next();
 +
    print( cotxe.marca ) ;
 +
}
 +
</syntaxhighlight>
 +
 +
O bé (foreach asíncron):
 +
<syntaxhighlight lang="javascript">
 +
db.cotxes.find().forEach(function (elem, index, array) {
 +
    print(elem.model);
 +
});
 +
</syntaxhighlight>
 +
 +
  
 
<br>
 
<br>
Línia 87: Línia 162:
  
 
=== Read / Select / Find ===
 
=== Read / Select / Find ===
 +
Referències:
 +
* http://docs.mongodb.org/manual/tutorial/query-documents/
 +
* Operadors: http://docs.mongodb.org/manual/reference/operator/query/
 +
* Ordenar resultats (sort): http://docs.mongodb.org/manual/reference/method/cursor.sort/
 +
 
La operació de query més estàndard en MongoDB és FIND. Alguns exemples:
 
La operació de query més estàndard en MongoDB és FIND. Alguns exemples:
 
  > db.<coleccio>.findOne()
 
  > db.<coleccio>.findOne()
Línia 96: Línia 176:
  
 
Si ho volem veure una mica més ben formatat:
 
Si ho volem veure una mica més ben formatat:
  > db.<col>.find().pretty()
+
  > db.<col>.find().'''pretty()'''
  
 
El 2n argument és per seleccionar camps (i excloure):
 
El 2n argument és per seleccionar camps (i excloure):
  > db.<col>.find( {"tipus":"examen",nota:50}, {"estudiant":true,"_id":false} )
+
  > db.<col>.find( {"tipus":"examen",nota:50}, '''{"estudiant":true,"_id":false}''' )
  
 
Operadors: estudiants amb examens amb notes > 90
 
Operadors: estudiants amb examens amb notes > 90
  > db.<col>.find( {"tipus":"examen", nota: {$gt:90} }, {"estudiant":true,"_id":false} )
+
  > db.<col>.find( '''{"tipus":"examen", nota: {$gt:90}''' }, {"estudiant":true,"_id":false} )
  
 
Estudiants amb notes entre 65 i 71:
 
Estudiants amb notes entre 65 i 71:
db.grades.find( {"type":"exam",score: {$gte:65,$lte:71}} ).sort({"score":1})
+
> db.grades.find( {"type":"exam",'''score:{$gte:65,$lte:71}}''' ).sort({"score":1})
  
Altres operadors: http://docs.mongodb.org/manual/reference/operator/query/
+
Per triar un sol element d'una llista podem utilitzar l'operador [] (ens mostrarà el 15è element):
 +
> db.countries.find()'''[15]'''
 +
> // ...o el que es el mateix...
 +
> db.countries.find().skip(15).limit(1)
  
TODO: Unir dues queries amb $or...
+
Exemples de AND i OR a : http://docs.mongodb.org/manual/tutorial/query-documents/
  
Ordenar resultats (sort): http://docs.mongodb.org/manual/reference/method/cursor.sort/
+
AND: el podem fer col·locant les diferents condicions dins el mateix diccionari:
 +
> db.<col>.find( {"tipus":"examen", nota:50} )
 +
OJU: no es pot aplicar el mateix camp 2 cops!!
 +
 
 +
OR: cal utilitzar l'operador $or:
 +
> db.cosa1.find({$or:[{"a":{$gt:89}},{"a":{$lt:10}}] })
 +
 
 +
Una discussió interessant sobre la millor manera de buscar [http://stackoverflow.com/questions/7811163/in-mongodb-how-do-i-find-documents-where-array-size-is-greater-than-1 arrays que tinguin més d'un nombre determinat d'elements].
 +
 
 +
Buscar un element dins d'un array. Per exemple, un tag dintre d'una llista (suposant que "tags" és un array):
 +
> db.posts.find( '''{"tags":"cooking"}''' )
 +
Ens mostrarà TOTS els elements que tinguin "cooking" dintre de la llista "tags".
 +
 
 +
Si volem filtrar els elements que tinguin algun dels ''tags'' que volem, podem utilitzar l''''operador $in''':
 +
> db.posts.find( {"tags":'''{$in:["cooking","veggie"]}'''} )
 +
 
 +
Si volem saber si existeix un element d'un diccionari (p.ex., tots els posts que tinguin l'element categoria, sense que ens importi el valor d'aquest):
 +
> db.posts.find( {"category":'''{$exists:true}'''} )
 +
 
 +
==== Regex ====
 +
 
 +
Segurament trobarem a faltar l'operador LIKE de SQL. En el cas de MongoDB hem de fer servir '''regular expressions''', un standard de com representar cadenes de caràcters. Per exemple, si busquem en la BD de pel·lícules:
 +
> db.movies.find( {title:/Once/} )
 +
 
 +
O el què seria el mateix:
 +
> db.movieDetails.find( {title:{$regex:"once"} } )
 +
 
 +
Per saber més de com utilitzar regular expressions en MongoDB:
 +
* [https://www.guru99.com/regular-expressions-mongodb.html Tutorial simple].
 +
* [https://docs.mongodb.com/manual/reference/operator/query/regex/#op._S_regex Mongo Regex Doc]
  
 
<br>
 
<br>
Línia 117: Línia 229:
 
=== Update ===
 
=== Update ===
 
Referències:
 
Referències:
* Mongo Shell Update: http://docs.mongodb.org/manual/tutorial/modify-documents/
+
* [http://docs.mongodb.org/manual/tutorial/modify-documents/ Mongo Shell Update]
* Pymongo Update: http://api.mongodb.org/python/current/api/pymongo/collection.html#pymongo.collection.Collection.update
+
* [http://docs.mongodb.org/manual/reference/method/db.collection.update/#db.collection.update Update reference]
* Update operators: http://docs.mongodb.org/manual/reference/operator/update/
+
** [http://docs.mongodb.org/manual/reference/operator/update-field/ Update d'un element].
 +
** [http://docs.mongodb.org/manual/reference/operator/update-array/ Update d'un array 1].
 +
** [http://docs.mongodb.org/manual/reference/operator/update/positional/#update Update d'un array 2]
 +
* [http://docs.mongodb.org/manual/reference/operator/update/ Update operators]
 +
* [http://api.mongodb.org/python/current/api/pymongo/collection.html#pymongo.collection.Collection.update Pymongo Update]
  
La operació de Update es pot resoldre de 2 maneres:  
+
Update d'un element:
# db.<col>.update( ... )  ...amb els següents operadors http://docs.mongodb.org/manual/reference/operator/update/#id1
+
> db.<col>.update( <query> , <update>, <options> )
 +
 
 +
La operació de ''Update'' d'un element es pot resoldre de 2 maneres:  
 +
# A lo bèstia: modifiquem el document i el tornem a inserir sencer:
 +
#: <pre>db.<col>.save( <doc> )</pre>
 +
#: ...on <doc> ha de ser el nou document modificat amb el "_id" (PK) pertinent
 +
# Modificant l'element concret amb la query:
 +
#: db.<col>.update( ... )  ...amb els següents operadors http://docs.mongodb.org/manual/reference/operator/update/#id1
 
#: Exemple en mongo shell:<pre>self.posts.update( {"_id":33}, {$addToSet:{"comments":comment}} )</pre>
 
#: Exemple en mongo shell:<pre>self.posts.update( {"_id":33}, {$addToSet:{"comments":comment}} )</pre>
 
#: Exemple en pymongo:<pre>self.posts.update( {"_id":33}, {"$addToSet":{"comments":comment}} )</pre>
 
#: Exemple en pymongo:<pre>self.posts.update( {"_id":33}, {"$addToSet":{"comments":comment}} )</pre>
# db.<col>.save( <doc> ) ...on <doc> ha de ser el nou document modificat amb el "_id" (PK) pertinent
+
 
 +
 
 +
Per actualitzar un element d'un array [http://docs.mongodb.org/manual/reference/operator/update/positional/#examples mira't aquest exemple].
 +
 
 +
Un altre exemple per actualitzar un element d'un array. Imaginem un taulell d'escacs (partida) amb una llista de figures (color,tipus,fila,col). Per moure-la:
 +
> db.partides.update({_id:ObjectId("55142510016e730fbeab7d33"),"figures.fila":0,"figures.col":7},{$set:{"figures.$.col":0}})
 +
Si tot va bé, ens dirà el nº d'elements trobats i modificats.
 +
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
 +
 
 
<br>
 
<br>
  
 
=== Delete / Remove ===
 
=== Delete / Remove ===
 +
Per esborrar una col·lecció sencera (OJU) utilitzem DROP:
 +
> db.<col>.drop()
 +
 +
Per esborrar un document, la instrucció és REMOVE:
 +
> db.<col>.remove( <condicio> )
 +
On <condicio> és una cerca com la que utilitzem a find. Per exemple, per eliminar un sol element:
 +
> db.imatges.remove( {_id: 1332} )
 +
 
Referències:
 
Referències:
 
* Pymongo remove: http://api.mongodb.org/python/current/api/pymongo/collection.html#pymongo.collection.Collection.remove
 
* Pymongo remove: http://api.mongodb.org/python/current/api/pymongo/collection.html#pymongo.collection.Collection.remove
 
* ...
 
* ...
 +
 +
<br>
 +
 +
=== Set/Insert elements d'un array ===
 +
Suposem un document amb elements dins d'un array:
 +
> db.chat.find().pretty()
 +
{
 +
"_id" : ObjectId("58dd35346b44f5babaf8f187"),
 +
"chatid" : 1234,
 +
"enquestes" : [
 +
{
 +
"eid" : 1
 +
},
 +
{
 +
"eid" : 3
 +
}
 +
]
 +
}
 +
 +
Podem modificar els objectes d'un element de l'array. La query seria aquesta:
 +
> db.chat.update( {"chatid":1234,"enquestes.eid":3} , {$set:{"enquestes.$.lala":"hola"}} )
 +
 +
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
 +
 +
El "quid" de la qüestió és utilitzar l'operador $, que ens substitueix l'element cercat a la part de la query (en aquest cas, "enquestes.eid":3 ).
 +
 +
Amb el què obtindríem el següent:
 +
 +
> db.chat.find().pretty()
 +
{
 +
"_id" : ObjectId("58dd35346b44f5babaf8f187"),
 +
"chatid" : 1234,
 +
"enquestes" : [
 +
{
 +
"eid" : 1
 +
},
 +
{
 +
"eid" : 3,
 +
"lala" : "hola"
 +
}
 +
]
 +
}
 +
 +
=== Arrays aniuats ===
 +
Una de les principals limitacions de MongoDB (a data d'Abril de 2017, v3.4) és la impossibilitat d'actualitzar arrays aniuats.
 +
 +
* http://stackoverflow.com/questions/18573117/updating-nested-arrays-in-mongodb-via-mongo-shell
 +
* https://jira.mongodb.org/browse/SERVER-831
 +
 +
La fortalesa de MongoDB resideix en el tractament dels diccionaris (arrays associatius) però no tant amb els "arrays posicionals" (els clàssics).
 +
 +
Si ens trobem en aquest tipus de situació haurem de canviar el nostre tipus de document per evitar l'aniuament.
  
 
<br>
 
<br>
Línia 211: Línia 402:
  
 
OJU, amb aquests perquè poden arribar a ocupar molt espai, afectant a la performance de les insercions.
 
OJU, amb aquests perquè poden arribar a ocupar molt espai, afectant a la performance de les insercions.
 +
 +
<br>
 +
 +
== Pre-joined data ==
 +
Una característica important que cal entendre de MongoDB és que les dades les situem PRE-JOINED. És a dir que, a diferència de SQL, ja venen combinades (incrustades) dins del mateix document. Això repercuteix en:
 +
* Major localització de les dades, i major velocitat de cerca.
 +
* Com es fan les queries
 +
 +
Per entendre-ho, fes un cop d'ull a aquestes dues maneres de guardar la informació:
 +
* [[Fitxer:Notes.js]]
 +
* [[Fitxer:Alumnes.js]]
 +
Descarrega els arxius i importa'ls dins la BD "notes". El 1r a la col·lecció "notes" i el 2n a la col·lecció "alumnes". ULL, perquè les dades dels dos arxius són completament diferents.
 +
 +
PRE-JOINED data (Alumnes.js):
 +
<syntaxhighlight lang="javascript">
 +
> db.students.findOne()
 +
{
 +
"_id" : ObjectId("50b59cd75bed76f46522c351"),
 +
"student_id" : 0,
 +
"class_id" : 16,
 +
"scores" : [
 +
{
 +
"type" : "exam",
 +
"score" : 59.1805667559299
 +
},
 +
{
 +
"type" : "quiz",
 +
"score" : 47.58960202938239
 +
},
 +
{
 +
"type" : "homework",
 +
"score" : 6.48470951607214
 +
},
 +
{
 +
"type" : "homework",
 +
"score" : 68.33519637418685
 +
},
 +
{
 +
"type" : "homework",
 +
"score" : 78.53068038180965
 +
}
 +
]
 +
}
 +
</syntaxhighlight>
 +
 +
UN-JOINED data (Notes.js):
 +
<syntaxhighlight lang="javascript">
 +
> db.grades.find().pretty()
 +
{
 +
"_id" : ObjectId("50906d7fa3c412bb040eb577"),
 +
"student_id" : 0,
 +
"type" : "exam",
 +
"score" : 54.6535436362647
 +
}
 +
{
 +
"_id" : ObjectId("50906d7fa3c412bb040eb57b"),
 +
"student_id" : 1,
 +
"type" : "exam",
 +
"score" : 74.20010837299897
 +
}
 +
{
 +
"_id" : ObjectId("50906d7fa3c412bb040eb57a"),
 +
"student_id" : 0,
 +
"type" : "homework",
 +
"score" : 63.98402553675503
 +
}
 +
...
 +
</syntaxhighlight>
 +
 +
Fes un findOne() de cadascun i observa les diferències. Com pots veure, el 2n té les dades de les qualificacions incrustades. Aquest és l'estil de document que acostumarem a tenir en MongoDB: una col·lecció de documents sobre (en aquest cas) alumnes.
 +
 +
El següent apartat parla d'agrupacions i càlculs amb ''aggregation framework''. Per anar veient la diferència entre les dues formes de tenir les dades, farem una senzilla agrupació. Plantejem 2 ''queries'':
 +
# total de notes d'examens
 +
# total de documents de cada tipus (exam, homework, quiz)
 +
 +
<br>
 +
 +
=== Exemple doc UN-joined ===
 +
La 1a ''query'' pel primer cas és molt senzill (dades no aniuades):
 +
> db.notes.find( {type:"exam"} ).count()
 +
 +
Però la 2a pregunta ja no la podem resoldre en una sola ''query'' simple. Caldrà fer-ne 3, i cal saber ''a priori'' quins diferents tipus de "scores" tinc als documents. L'alternativa és utilitzar el ''aggregation framework'':
 +
 +
<syntaxhighlight lang="javascript">
 +
db.notes.aggregate({
 +
    $group: {
 +
        _id:"$type",
 +
        total: {$sum:1}
 +
        }
 +
    })
 +
</syntaxhighlight>
 +
 +
En format "compacte":
 +
> db.notes.aggregate( {$group:{_id:"$type",total:{$sum:1}}} )
 +
 +
=== Exemple doc PRE-joined ===
 +
Anem directament per la 2a query. La 1a serà un filtratge del cas "exam" un cop resolguem la 2a query.
 +
 +
El primer que ens cal és fer un UNJOIN de les dades (deixar-les com les del 1r exercici de notes). Això es fa amb l'operador "$unwind". Observa la sortida d'aquesta comanda:
 +
> db.alumnes.aggregate( {$unwind:"$scores"} )
 +
 +
Un cop UNJOINED les dades es poden agrupar
 +
> db.alumnes.aggregate( {$unwind:"$scores"} , {$group:{_id:"$scores.type",total:{$sum:1}}} )
 +
 +
Per comprovar el què estem dient, prova de fer l'agrupament per "scores.type" sense fer el $unwind:
 +
> db.alumnes.aggregate( {$group:{_id:"$scores.type",total:{$sum:1}}} )
 +
 +
Què diríes que està passant?
  
 
<br>
 
<br>
Línia 261: Línia 560:
  
 
=== Aggregation Pipeline (teoria) ===
 
=== Aggregation Pipeline (teoria) ===
El "pipeline" és el procés que realitza ''aggregation'' per obtenir els resultats:
+
El "pipeline" és el procés que realitza ''aggregation'' per obtenir els resultats. El podem considerar un tub on anem successivament processsant les dades.
 +
 
 
* Vídeo: http://www.youtube.com/watch?v=WazN2DS8s8c&list=PLOqwUgCzNJcsC1ARX93IZZLf9k3rVRrCn
 
* Vídeo: http://www.youtube.com/watch?v=WazN2DS8s8c&list=PLOqwUgCzNJcsC1ARX93IZZLf9k3rVRrCn
 
* Doc: http://docs.mongodb.org/manual/core/aggregation-pipeline/
 
* Doc: http://docs.mongodb.org/manual/core/aggregation-pipeline/
Línia 274: Línia 574:
 
* $skip (n:1)
 
* $skip (n:1)
 
* $limit (n:1)
 
* $limit (n:1)
* $unwind: (1:n) genera una entrada per cada element d'array, si existeix un array incrustat en un document.
+
* '''$unwind''': (1:n) genera una entrada per cada element d'array, si existeix un array incrustat en un document.
  
Fixeu-vos en què l'única que genera més entrades es la darrera: unwind (les altres o les deixa igual -sort- o bé les redueix).
+
Per $project i $match podem utiltizar els [http://docs.mongodb.org/manual/reference/operator/query/ operadors] vistos en les querys simples (find).
 +
 
 +
Fixeu-vos en què l'única que genera més entrades es la darrera: unwind (les altres o les deixa igual -sort- o bé les redueix). És l'operador més característic d'una DB no-SQL, ja que ens permet desglossar els subdocuments que venen PRE-JOINED (incrustats).
 +
<br>
 +
 
 +
=== Pipeline query: exemple 1 (blog) ===
 +
Tenim una col·lecció de documents d'un blog, cada document té una estructura com aquesta:
 +
 
 +
<syntaxhighlight lang="javascript">
 +
{
 +
"_id" : ObjectId("50ab0f8bbcf1bfe2536dc3f8"),
 +
"body" : "Lorem ipsum dolor sit amet,... \n",
 +
"permalink" : "TqoHkbHyUgLyCKWgPLqm",
 +
"author" : "machine",
 +
"title" : "US Constitution",
 +
"tags" : [
 +
"trade",
 +
"fowl",
 +
"forecast",
 +
"pest",
 +
"professor",
 +
"willow",
 +
"rise",
 +
"brace",
 +
"ink",
 +
"road"
 +
],
 +
"date" : ISODate("2012-11-20T05:05:15.229Z")
 +
"comments" : [
 +
{
 +
"body" : "blablabla...",
 +
"email" : "[email protected]",
 +
"author" : "Linnie Weigel"
 +
},
 +
{
 +
"body" : "blebleble...",
 +
"email" : "[email protected]",
 +
"author" : "Sadie Jernigan"
 +
}
 +
        ]
 +
}
 +
</syntaxhighlight>
 +
 
 +
Les dades, com sol ser habitual en MongoDB, estan PREJOINED (comentaris dins dels posts).
 +
 
 +
Observa la següent consulta:
 +
<syntaxhighlight lang="javascript">
 +
db.posts.aggregate(
 +
    {$project: {
 +
        "comments.author":1
 +
    }},
 +
    {$unwind:"$comments"},
 +
    {$group:
 +
    {
 +
_id: {
 +
    "auth":"$comments.author"
 +
},
 +
total:{$sum:1}
 +
    }
 +
    },
 +
    {$sort: {
 +
        "total":1
 +
    }}
 +
)</syntaxhighlight>
 +
 
 +
...ens realitza els següents processos:
 +
# (project): sel·lecciona només els autors dels comentaris als posts
 +
# (unwind): desglossa cadascuna de les entrades (comentaris, que contenen un array d'autors de comentari) en un sol array amb elements
 +
# (group) agrupa per autor i calcula el nombre de cops que apareix cadascun
 +
# (sort): ordena per ordre creixent d'aparicions
 +
 
 +
=== Pipeline query: exemple 2 (zipcodes) ===
 +
Aquesta utiltiza les [http://media.mongodb.org/zips.json dades dels codis postals dels USA].
 +
 
 +
Calcularem la mitjana de les poblacions de California (CA) i New York (NY) (or lògic!) que tinguin més de 25000 habitants.
 +
 
 +
Les dades estan per districte postal (zipcode), pel que les poblacions estan separades en districtes. Abans que res caldrà agrupar-los per obtenir les dades de població total de cada ciutat.
 +
 
 +
<syntaxhighlight lang="javascript">
 +
db.zipcodes.aggregate(
 +
    /* agrupem per sumar la poblacio dels diferents districtes de cada ciutat */
 +
    {$group: {
 +
        _id: {
 +
            "city":"$city",
 +
            "state":"$state"
 +
        },
 +
        total_pop:{$sum:"$pop"}
 +
    }},
 +
    /* filtrem les ciutats amb poblacio>25000 i de CA i NY */
 +
    {$match: {
 +
        total_pop:{$gt:25000},
 +
        $or: [{"_id.state":"CA"},{"_id.state":"NY"}]
 +
    }},
 +
    /* agrupem i calculem mitjana */
 +
    {$group: {
 +
        _id: {},
 +
        mitjana:{$avg:"$total_pop"}
 +
    }}
 +
)
 +
</syntaxhighlight>
 +
 
 +
=== Exemples típics per pipeline queries ===
 +
Per provar exemples de ''aggregate'' és més adient utilitzar una BD pre-joined com [[Fitxer:Alumnes.js]]
 +
 
 +
 
 +
==== Mostrar un camp (SELECT) ====
 +
Si només volem mostrar un(s) camp(s) determinats ($project):
 +
> db.students.aggregate( {$project:{_id:0,student_id:1,class_id:1}} )
 +
 
 +
==== Mitjana de notes d'un estudiant ====
 +
Etapes:
 +
# $match: seleccionem estudiant
 +
# $unwind: partim el doc en tants com elements té l'array
 +
# $group: calculem mitjana
 +
> db.students.aggregate( {$match:{student_id:0,class_id:16}} , {$unwind:"$scores"}, {$group:{_id:"",mitjana:{$avg:"$scores.score"}}} )
 +
 
 +
Per veure com evoluciona la query, prova afegint etapa per etapa:
 +
 
 +
 
 +
==== Recompte de TOTS els elements ====
 +
Agrupem per "res" (en la última etapa):
 +
> db.col.aggregate( {...} , {...} , '''{$group:{_id:"",count:{$sum:1}}}''' )
 +
 
 +
Prova de calcular la mitjana.
 +
 
 +
==== Error "Exceeded memory limit for $group" ====
 +
Sol passar a partir de la v 2.6, ja que no utilitza el disc dur per resoldre les operacions. Cal afegir-hi la '''opció allowDiskUse=true''' [http://docs.mongodb.org/manual/reference/method/db.collection.aggregate/#perform-large-sort-operation-with-external-sort com en aquest exemple].
 +
 
 +
Fixa't en què per poder afegir paràmetres ja no podem utilitzar la forma típica del pipeline:
 +
> <strike>db.col.aggregate( {...} , {...} , ... )</strike>
 +
 
 +
Caldrà que utilitzem aquesta, amb un array [] per la pipeline i un objecte {} amb els paràmetres (pel nostre cas, allowDiskUse):
 +
> db.col.aggregate( [ {...} , {...} , ... ] , {allowDiskUse:true} )
 +
 
 +
==== Fer un "explain" per mostrar metadades (indexes) ====
 +
> db.col.aggregate( [ {...} , {...} , ... ] , {explain:true} )
  
 
<br>
 
<br>
  
 
== Exercicis ==
 
== Exercicis ==
...
+
Arxius de dades per proves:
 +
* http://media.mongodb.org/zips.json
 +
* [[Fitxer:Products.js]]
 +
* [[Fitxer:notes.js]]
 +
* [[Fitxer:alumnes.js]]
 +
* [http://cacauet.org/posts.json Blog posts] (oju, 35MB)
 +
* Dades de països: https://github.com/mledoze/countries/blob/master/countries.json
 +
* Bios collection: http://docs.mongodb.org/manual/reference/bios-example-collection/
 +
* Imatges i albums: [http://cacauet.org/images.json images.json] / [[Fitxer:albums.json]]
 +
* [http://infla.cat/wiki/enron.zip Emails Enron] : aquesta l'heu d'importar amb '''mongorestore''' (no amb mongoimport com les altres en format json)
 +
<br>
 +
 
 +
=== Importació ===
 +
# Agafa el link de les dades de països en format JSON i adapta'l convenientment perquè puguem importar-ho a la nostra BBDD mongoDB.<pre> https://github.com/mledoze/countries/blob/master/countries.json</pre>
 +
#* Pistes pel document JSON:
 +
#** Utiltiza un processador de text estil ''geany'' que pugui tractar cerques i substitucions amb seqüències d'escapament (Ex.:"\n") i/o expressions regular (regexp).
 +
#** Un document JSON per mongoDB ha de contenir una entrada (diccionari {}) per línia, sense separació per comes entre línies
 +
#** Eliminar tots els salts de línia ("\n") i els tabuladors ("\t")
 +
#** Després introduïr només els salts de línia necessaris, és a dir, quan hi ha canvi de país, amb "},{". Canviarem la coma per un salt de línia.
 +
#** Us han d'entrar uns 250 països.
 +
# Importa els diferents arxius disponibles més amunt en diferentes bases de dades, menys la d'imatges i àlbums, que cal posar-los a la mateixa BBDD en diferents col·leccions.
 +
 
 +
=== JavaScript ===
 +
Fes un script que ens generi documents amb una estructura com aquesta (mínim 30 articles):
 +
 
 +
OJU: els articles i el seu preu han de ser coherents!
 +
<syntaxhighlight lang="javascript">
 +
{
 +
    "compra_id": 18,
 +
    "client_id": 5,
 +
    "article": "pera", // article a triar entre [pera, poma, préssec]
 +
    "quantitat": 1.56, // nombre aleatori entre 0.5 i 3
 +
    "preu": 2.03      // preu en correspondència a l'article triat
 +
}
 +
</syntaxhighlight>
 +
 
 +
Inspecciona la comanda '''mongoexport''' i exporta les dades en un arxiu .js
 +
 
 +
=== Filtratge simple (find) ===
 +
# Amb la BBDD de països: [http://cacauet.org/alumnes/index.php/MongoDB:_countries_examples respostes aquí]
 +
## Llista de noms de països.
 +
## Nombre total de països.
 +
## Nombre total de països que parlen anglès.
 +
## Llistat de països que parlen anglès a Europa.
 +
## Nombre total de països que parlen anglès fora d'Europa.
 +
## Nombre total de països que parlen anglès com a única llengua.
 +
## Llista de països que parlen català (amb display tabulat).
 +
## Llista de països que parlen espanyol (només mostrar el nom del país i les llengües parlades).
 +
## Llista de països que parlen espanyol i anglès.
 +
## Llista de països que parlen francès i anglès (mostrar país, llengües i continent).
 +
## Llista de països amb una "j" al nom (case insensitive).
 +
##: Mireu aquesta referència (regexp) http://docs.mongodb.org/manual/reference/operator/query/regex/#op._S_regex
 +
## Llista de països el nom del qual comença per "j" (case insensitive)
 +
## Nom nadiu del país que en espanyol (ES) anomenen "Islandia".
 +
## Llista de països amb 4 o més llengües oficials.
 +
## ...
 +
# Amb la BBDD de les '''qualificacions''' d'alumnes: [[Fitxer:notes.js]] | [http://cacauet.org/alumnes/index.php/MongoDB:_grades_examples respostes aquí]
 +
## Llistat ascendent de notes
 +
##: Resposta: 51.70205387618566
 +
## Estudiant (student_id) amb nota més alta d'un examen.
 +
##: Pista: llistat descendent de notes.
 +
##: student_id=176 (score=99.96723280505422)
 +
## Tipus de nota que té la qualificació més alta.
 +
##: exam
 +
## Nota més baixa en un quiz.
 +
##:
 +
## Quantitat de "homeworks" totals.
 +
## Quantitat de "homeworks" per l'estudiant student_id=3
 +
 
 +
=== Pipeline queries (aggregation framework) ===
 +
Per poder realitzar aquest apartat has hagut de llegir-te exhaustivament l'apartat "aggregation framework".
 +
 
 +
Mira't els exemples anteriors i pensa que també en tens:
 +
* [http://docs.mongodb.org/manual/tutorial/aggregation-zip-code-data-set/ un tutorial utilitzant els codis postals dels USA].
 +
 
 +
 
 +
# Amb les qualificacions dels alumnes: [[Fitxer:alumnes.js]] | [http://www.cacauet.org/alumnes/index.php/MongoDB:_qualifications_examples_%28aggregation%29 respostes aquí] | [https://infla.cat/wiki/MongoDB_aggregation_queries_-_alumnes o aquí (infla)]
 +
## Llistat de notes d'examens (utiltiza $unwind)
 +
## Mitjana de totes les notes
 +
##: Resposta = 49.25
 +
## Mitjana dels examens
 +
##: Resposta = 50.83
 +
## Mitjana de totes les notes de la classe_id = 5
 +
##: Resposta = 54.16
 +
## Mitjana dels examens de la classe_id = 3
 +
##: Resposta = 49.09
 +
## Alumne amb millor mitjana (llista de mitjana de notes per alumne ordenada)
 +
##: Resposta 1 (suposant alumnes diferents a cada classe) : alumne 3 de la classe 11, mitjana = 86.87
 +
##: Resposta 2 (suposant alumne_id com a alumne únic) : alumne 44, mitjana = 65.57
 +
## Classe amb pitjor mitjana (llista de mitjana de notes per classe ordenada descendent)
 +
##: Resposta = classe 2 amb mitjana 42.59
 +
## Llista ordenada de mitjana d'examens per classe amb recompte del nº d'examens per classe.
 +
##: Millor i pitjor resultat?
 +
##: Millor = classe 20 amb 70.87 (7 examens)
 +
# Amb [[Fitxer:Countries2014.json.txt]] la BBDD de països 2014 | [http://www.cacauet.org/alumnes/index.php/MongoDB:_countries_examples_%28aggregation%29 respostes aquí] | [https://infla.cat/wiki/MongoDB_aggregation_queries_-_pa%C3%AFsos o aquí (infla)]
 +
#: (ull, la de la web ja no ens serveix perquè l'estructura actual de les dades no ens permet certs càlculs)
 +
## Llista de continents i la seva població.
 +
## Llista dels països que no tenen continent (="") ordenada per població.
 +
## Nombre de països amb anglès com a única llengua oficial.
 +
## Nombre de països amb francès com llengua oficial (poden tenir altres).
 +
## Llista ordenada de països segons el nombre de llengües oficials.
 +
## Llista de països amb 4 o més llengües oficials.
 +
## Quin és el país amb més llengües oficials?
 +
## Recompte de països segons el seu nombre de llengües oficials.
 +
## Llista de llengües (language o languageCodes) ordenades per la quantitat de països que les tenen com a oficials.
 +
## Llista de llengües (language o languageCodes) ordenades pel total de gent que la parla (oficialment).
 +
## Llista "top ten" de països amb major nombre de fronteres.
 +
## Llista de monedes amb el nombre de països que les parlen. Ordena-la descendentment.
 +
## Llista de monedes amb el llistat de continents on s'utilitzen. Ordena-la descendentment.
 +
## Llista de contintents amb el nombre de països que el formen. Ordena-la descendentment.
 +
## Llista de contintents amb el nombre de llengües que es parlen. Ordena-la descendentment.
 +
## Llista de subregions ordenades pel nombre de països que tenen. Ordena-la descendentment.
 +
## Llista de subregions ordenades pel nombre d'habitants. Ordena-la descendentment.
 +
## ...
 +
# Amb els [http://media.mongodb.org/zips.json zips (codis postals dels USA)]:
 +
## Removing Rural Residents: In this problem you will calculate the number of people who live in a zip code in the US where the city starts with a digit. We will take that to mean they don't really live in a city.
 +
##: Resposta: 298015
 +
## ...
 +
# Amb els posts del blog: [http://cacauet.org/posts.json Blog posts]
 +
## ...
 +
## ...
 +
# Amb les imatges i àlbums: [http://cacauet.org/images.json images.json] / [[Fitxer:albums.json]]
 +
## Esborrar les imatges que no estan referenciades en cap àlbum (cal fer un script en JS).
 +
## ...

Revisió de 15:58, 2 març 2020

MongoDB és una base de dades no-SQL, orientada a documents i principalment utilitzada per aplicacions web, buscant rendiments alts i escalables.

Altres articles relacionats:


Intro No-SQL[modifica]

Algunes característiques:

  • Orientada a documents.
  • La inconsistència de dades deixa de ser una prioritat.
  • Treballem amb incrustació (embedding). Exemple de blog: posts+comments
    • Dades relatives al document incrustades en el mateix (exemple de blog amb post+comment). Això ajuda a mantenir certa consistència.
    • Millora la velocitat d'accés (performance). Latència dels discs alta (1ms) però ample de banda alt (BW).
  • No té FKs. En canvi té operacions atòmiques.
  • No té transaccions (consistència temporal). Alternatives:
    • Reestructurar la nosta BBDD per treballar en un sol tipus de document (schema).
    • Implementar-la en el nostre software.
    • Tolerar certa inconsistència temporal (dependrà del tipus d'aplicació).
  • Relacions:
    • 1:1 millor sempre unir-les en un sol doc a no ser que excedeixi 16 MB (maxim).
    • 1:N si "N" son molts millor apuntar dels molts al 1 (amb el _id). També es pot fer al revés amb una llista.
      Hi ha una variant típica de 1:pocs que és el cas que ens agrada per incrustar.
    • N:N (molts:molts) calen llistes (es poden posar a les 2 col·leccions tot i que això significa que la app ha de controlar la consistència
  • Sempre intentem incrustar docs = PRE-JOINED data

Info:

  • Schemaless and document based
  • No joins, no SQL
  • RDBS is not scalability in general machines
  • memcache is not highly functional but scales well
  • Joins don't scale well
  • Work with pre-joined data
  • Documents have a 16 MB limit on 32 bit OS's
  • Transactions are unavailable
  • Crud Operations exists as wire protocols
  • 'id' is immutable, and made up of Current Time, Machine Id, Process ID and global Counter
  • remove is not an isolated transaction.


Referències:


Primeres passes[modifica]

  • Monogo shell:
    $ mongo
  • Seleccionar una db (o crear):
    > use <nom_db>
  • Per saber les bases de dades que tenim:
    > show dbs
  • Un cop hem entrat en una DB amb "use", per saber les col·leccions:
    > show collections


JavaScript en la Mongo Shell[modifica]

Això ens facilita molt la creació i manipulació de les dades. Per exemple, per crear un munt de documents que continguin un "num" i una "cosa" a triar aleatòriament entre "tablet", "smartphone" i "xocolata":

for (i=0;i<100000;i++) {
   coses = ["tablet","smartphone","xocolata"]
   var elem = {
      "num": i,
      "cosa": coses[Math.floor(Math.random()*3)]
   }
   db.coses.save( elem )
}

Un altre exemple amb subdocuments:

for (i=0;i<1000;i++) {
   names=["examen","treball","questionari"];
   for(j=0;j<3;j++) {
      db.notes.insert({
         "estudiant":i,
         "tipus": names[j],
         nota : Math.round(Math.random()*100)
      });
   }
}

Si volem utilitzar els resultats d'una query en un script per mostrar dades o fer subqueries:

var img = db.images.findOne( {_id:33} );
// findOne ens torna un diccionari (array associatiu) JSON. Podem accedir als elements amb:
print( img["width"] );
print( img.width );
print( img );

ULL: això ens funciona bé per findOne() i per aggregate(), no per find().

Cursors[modifica]

Un .findOne() o un .aggregate() ens torna un objecte JSON amb la resposta demanada (veure més avall la sintaxi d'ambdós).

En canvi, un find() ens torna un cursor, amb el què hem d'acabar de realitzar l'extracció de les dades una a una, en un bucle:

var imgCursor = db.images.find( {"tags":"cats"} );
// per mostrar el 4rt element:
elem = imgCursor[4];
// Per obtenir el següent element:
var image = imgCursor.next();
// Per iterar per cadscun d'ells:
while( imgCursor.hasNext() ) {
    var image = imgCursor.next();
    print(image._id);
    // o bé:
    print(image["width"]);
    // per mostrar tot l'objecte (no sé perquè només surt 1. (TODO: arreglar-ho...)
    JSON.stringify( image );
    // o, només per mongoShell, directament:
    image;
}

Altres maneres d'iterar una col·lecció (foreach síncron):

for( var iter=db.cotxes.find(); iter.hasNext();  ) {
    var cotxe = iter.next();
    print( cotxe.marca ) ;
}

O bé (foreach asíncron):

db.cotxes.find().forEach(function (elem, index, array) {
    print(elem.model);
});



CRUD[modifica]

Create Read Update Delete


Create / Insert / Save[modifica]

Per crear documents dins d'una col·lecció:

> db.<coleccio>.save( <JSON_obj> )


Read / Select / Find[modifica]

Referències:

La operació de query més estàndard en MongoDB és FIND. Alguns exemples:

> db.<coleccio>.findOne()
> db.<coleccio>.findOne( {"tipus":"examen"} )
> db.<coleccio>.find()
> db.<coleccio>.find( {estudiant:103} )

OJU: en Pymongo és find_one enlloc de findOne

Si ho volem veure una mica més ben formatat:

> db.<col>.find().pretty()

El 2n argument és per seleccionar camps (i excloure):

> db.<col>.find( {"tipus":"examen",nota:50}, {"estudiant":true,"_id":false} )

Operadors: estudiants amb examens amb notes > 90

> db.<col>.find( {"tipus":"examen", nota: {$gt:90} }, {"estudiant":true,"_id":false} )

Estudiants amb notes entre 65 i 71:

> db.grades.find( {"type":"exam",score:{$gte:65,$lte:71}} ).sort({"score":1})

Per triar un sol element d'una llista podem utilitzar l'operador [] (ens mostrarà el 15è element):

> db.countries.find()[15]
> // ...o el que es el mateix...
> db.countries.find().skip(15).limit(1)

Exemples de AND i OR a : http://docs.mongodb.org/manual/tutorial/query-documents/

AND: el podem fer col·locant les diferents condicions dins el mateix diccionari:

> db.<col>.find( {"tipus":"examen", nota:50} )

OJU: no es pot aplicar el mateix camp 2 cops!!

OR: cal utilitzar l'operador $or:

> db.cosa1.find({$or:[{"a":{$gt:89}},{"a":{$lt:10}}] })

Una discussió interessant sobre la millor manera de buscar arrays que tinguin més d'un nombre determinat d'elements.

Buscar un element dins d'un array. Per exemple, un tag dintre d'una llista (suposant que "tags" és un array):

> db.posts.find( {"tags":"cooking"} )

Ens mostrarà TOTS els elements que tinguin "cooking" dintre de la llista "tags".

Si volem filtrar els elements que tinguin algun dels tags que volem, podem utilitzar l'operador $in:

> db.posts.find( {"tags":{$in:["cooking","veggie"]}} )

Si volem saber si existeix un element d'un diccionari (p.ex., tots els posts que tinguin l'element categoria, sense que ens importi el valor d'aquest):

> db.posts.find( {"category":{$exists:true}} )

Regex[modifica]

Segurament trobarem a faltar l'operador LIKE de SQL. En el cas de MongoDB hem de fer servir regular expressions, un standard de com representar cadenes de caràcters. Per exemple, si busquem en la BD de pel·lícules:

> db.movies.find( {title:/Once/} )

O el què seria el mateix:

> db.movieDetails.find( {title:{$regex:"once"} } )

Per saber més de com utilitzar regular expressions en MongoDB:


Update[modifica]

Referències:

Update d'un element:

> db.<col>.update( <query> , <update>, <options> )

La operació de Update d'un element es pot resoldre de 2 maneres:

  1. A lo bèstia: modifiquem el document i el tornem a inserir sencer:
    db.<col>.save( <doc> )
    ...on <doc> ha de ser el nou document modificat amb el "_id" (PK) pertinent
  2. Modificant l'element concret amb la query:
    db.<col>.update( ... ) ...amb els següents operadors http://docs.mongodb.org/manual/reference/operator/update/#id1
    Exemple en mongo shell:
    self.posts.update( {"_id":33}, {$addToSet:{"comments":comment}} )
    Exemple en pymongo:
    self.posts.update( {"_id":33}, {"$addToSet":{"comments":comment}} )


Per actualitzar un element d'un array mira't aquest exemple.

Un altre exemple per actualitzar un element d'un array. Imaginem un taulell d'escacs (partida) amb una llista de figures (color,tipus,fila,col). Per moure-la:

> db.partides.update({_id:ObjectId("55142510016e730fbeab7d33"),"figures.fila":0,"figures.col":7},{$set:{"figures.$.col":0}})

Si tot va bé, ens dirà el nº d'elements trobats i modificats.

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })


Delete / Remove[modifica]

Per esborrar una col·lecció sencera (OJU) utilitzem DROP:

> db.<col>.drop()

Per esborrar un document, la instrucció és REMOVE:

> db.<col>.remove( <condicio> )

On <condicio> és una cerca com la que utilitzem a find. Per exemple, per eliminar un sol element:

> db.imatges.remove( {_id: 1332} )

Referències:


Set/Insert elements d'un array[modifica]

Suposem un document amb elements dins d'un array:

> db.chat.find().pretty()
{
	"_id" : ObjectId("58dd35346b44f5babaf8f187"),
	"chatid" : 1234,
	"enquestes" : [
		{
			"eid" : 1
		},
		{
			"eid" : 3
		}
	]
}

Podem modificar els objectes d'un element de l'array. La query seria aquesta:

> db.chat.update( {"chatid":1234,"enquestes.eid":3} , {$set:{"enquestes.$.lala":"hola"}} )

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

El "quid" de la qüestió és utilitzar l'operador $, que ens substitueix l'element cercat a la part de la query (en aquest cas, "enquestes.eid":3 ).

Amb el què obtindríem el següent:

> db.chat.find().pretty()
{
	"_id" : ObjectId("58dd35346b44f5babaf8f187"),
	"chatid" : 1234,
	"enquestes" : [
		{
			"eid" : 1
		},
		{
			"eid" : 3,
			"lala" : "hola"
		}
	]
}

Arrays aniuats[modifica]

Una de les principals limitacions de MongoDB (a data d'Abril de 2017, v3.4) és la impossibilitat d'actualitzar arrays aniuats.

La fortalesa de MongoDB resideix en el tractament dels diccionaris (arrays associatius) però no tant amb els "arrays posicionals" (els clàssics).

Si ens trobem en aquest tipus de situació haurem de canviar el nostre tipus de document per evitar l'aniuament.


Indexes[modifica]

Els indexes augmenten la velocitat de les consultes. Si no fos per ells, cada cop que fem una recerca (find) hauriem de repassar tota la BBDD, i si aquesta té una xifra elevada de registres (diguem-ne, 10 milions) la consulta serà lenta, i farà que el sistema sigui impracticable.

Referència: http://docs.mongodb.org/manual/indexes/

Vídeos de MongoDB University:

  • Introducció als índexes (segueix la sèrie de vídeos, hi ha diversos d'una durada de pocs minuts cadascun).
    • Un índex accelera les cerques
    • El mateniment de l'índex alenteix les insercions.
  • Índexes i performance (velocitat d'accés).
    • findOne() és més ràpid perquè quan troba una mostra ja surt.
    • si el ID que busquem és baix va ràpid (al principi de la col·lecció).
    • Si el ID que busquem és alt triga més.
    • find() triga igual perquè ha de buscar en tota la base de dades: lent (sense índex)
    • Buscar un element que no existeix: triga molta estona.
    • Selectivitat: millor triar índexes que no es repeteixin gaire (millor performance). Per ex. millor el quilometratge d'un cotxe que el color.
  • ...

Comandes[modifica]

Per crear un índex (1 és ascendent i -1 descendent):

> db.<colleccio>.ensureIndex( {<camp1>:1,<camp2>:-1,...} , <opcions> }

Per destruïr-lo:

> db.<colleccio>.dropIndex({<camp1>:1,<camp2>:-1,...}}

Per saber els índexes de tot el sistema:

> db.system.indexes.find()

Per saber els índexes que té una col·lecció:

> db.<colleccio>.getIndexes()

Espai ocupat pels índex (ens importa per saber si es poden tenir a la RAM: performance):

> db.<colleccio>.getIndexSize()

Profiling (dades sobre el temps i indexes emprats per la consulta):

> db.<colleccio>.find(...).explain()

(temps = "millis")

Per forçar a utilitzar un índex determinat en una consulta a la Mongo Shell:

> db.<colleccio>.find(...).hint( {a:1} )

Oju, en pymongo es fa amb una llista, no amb un diccionari:

db.colleccio.find(...).hint( ['a', pymongo.ASCENDING] )

Si volem forçar a NO utilitzar cap índex:

> db.<colleccio>.find(...).hint( {$natural:1} )


Opcions[modifica]

Camp únic (unique):

> db.<colleccio>.ensureIndex( {<camp>:1} , {unique:true} }

dropDups: Si la col·lecció té duplicats al crear índex amb camp únic ens els carreguem: (no molt recomanat)

> db.<colleccio>.ensureIndex( {<camp>:1} , {unique:true, dropDups:true} }

Sparse indexes: podem indexar camps que no els tenen tots els documents (en principi seríen "null") => aquests documents no s'indexaran

> db.<colleccio>.ensureIndex( {<camp>:1} , {unique:true, sparse:true} }

OJU, perquè els SPARSE indexes poden donar lloc a comportaments diferents en les consultes depenent si hem creat l'índex o no. Per exemple:

> db.<colleccio>.find().sort( {"size":1} )

...els documents sense el camp size apareixeran si NO hem creat el sparse index. Si el creem, no apareixeran.


Multi-indexes[modifica]

Indexes sobre arrays, per ex, en documents com:

{
   title: "un post qualsevol",
   post: "cos del text",
   author: "enric",
   tags: ["btt","tennis","futbol"],
   categories: ["esports","hobbies"]
}

Es poden crear índexes sobre un array, però no sobre 2 alhora:

> ensureIndex( {tags:1} ) OK
> ensureIndex( {categories:-1} ) OK
> ensureIndex( {tags:1, categories:-1} )  NO ES POT

Posar un índex en un camp intern d'un document, per ex, en una col·lecció d'usuaris:

> ensureIndex( {adreces.telefons:1} )

(array d'adreces amb un camp telèfons incrustat)

OJU, amb aquests perquè poden arribar a ocupar molt espai, afectant a la performance de les insercions.


Pre-joined data[modifica]

Una característica important que cal entendre de MongoDB és que les dades les situem PRE-JOINED. És a dir que, a diferència de SQL, ja venen combinades (incrustades) dins del mateix document. Això repercuteix en:

  • Major localització de les dades, i major velocitat de cerca.
  • Com es fan les queries

Per entendre-ho, fes un cop d'ull a aquestes dues maneres de guardar la informació:

Descarrega els arxius i importa'ls dins la BD "notes". El 1r a la col·lecció "notes" i el 2n a la col·lecció "alumnes". ULL, perquè les dades dels dos arxius són completament diferents.

PRE-JOINED data (Alumnes.js):

> db.students.findOne()
{
	"_id" : ObjectId("50b59cd75bed76f46522c351"),
	"student_id" : 0,
	"class_id" : 16,
	"scores" : [
		{
			"type" : "exam",
			"score" : 59.1805667559299
		},
		{
			"type" : "quiz",
			"score" : 47.58960202938239
		},
		{
			"type" : "homework",
			"score" : 6.48470951607214
		},
		{
			"type" : "homework",
			"score" : 68.33519637418685
		},
		{
			"type" : "homework",
			"score" : 78.53068038180965
		}
	]
}

UN-JOINED data (Notes.js):

> db.grades.find().pretty()
{
	"_id" : ObjectId("50906d7fa3c412bb040eb577"),
	"student_id" : 0,
	"type" : "exam",
	"score" : 54.6535436362647
}
{
	"_id" : ObjectId("50906d7fa3c412bb040eb57b"),
	"student_id" : 1,
	"type" : "exam",
	"score" : 74.20010837299897
}
{
	"_id" : ObjectId("50906d7fa3c412bb040eb57a"),
	"student_id" : 0,
	"type" : "homework",
	"score" : 63.98402553675503
}
...

Fes un findOne() de cadascun i observa les diferències. Com pots veure, el 2n té les dades de les qualificacions incrustades. Aquest és l'estil de document que acostumarem a tenir en MongoDB: una col·lecció de documents sobre (en aquest cas) alumnes.

El següent apartat parla d'agrupacions i càlculs amb aggregation framework. Per anar veient la diferència entre les dues formes de tenir les dades, farem una senzilla agrupació. Plantejem 2 queries:

  1. total de notes d'examens
  2. total de documents de cada tipus (exam, homework, quiz)


Exemple doc UN-joined[modifica]

La 1a query pel primer cas és molt senzill (dades no aniuades):

> db.notes.find( {type:"exam"} ).count()

Però la 2a pregunta ja no la podem resoldre en una sola query simple. Caldrà fer-ne 3, i cal saber a priori quins diferents tipus de "scores" tinc als documents. L'alternativa és utilitzar el aggregation framework:

db.notes.aggregate({
    $group: {
        _id:"$type",
        total: {$sum:1}
        }
    })

En format "compacte":

> db.notes.aggregate( {$group:{_id:"$type",total:{$sum:1}}} )

Exemple doc PRE-joined[modifica]

Anem directament per la 2a query. La 1a serà un filtratge del cas "exam" un cop resolguem la 2a query.

El primer que ens cal és fer un UNJOIN de les dades (deixar-les com les del 1r exercici de notes). Això es fa amb l'operador "$unwind". Observa la sortida d'aquesta comanda:

> db.alumnes.aggregate( {$unwind:"$scores"} )

Un cop UNJOINED les dades es poden agrupar

> db.alumnes.aggregate( {$unwind:"$scores"} , {$group:{_id:"$scores.type",total:{$sum:1}}} )

Per comprovar el què estem dient, prova de fer l'agrupament per "scores.type" sense fer el $unwind:

> db.alumnes.aggregate( {$group:{_id:"$scores.type",total:{$sum:1}}} )

Què diríes que està passant?


Aggregation: group queries[modifica]

La llibreria aggregate ens permetrà fer consultes (queries) amb operacions com sum, avg, etc.

ULL: per la aggregate lib necessitem MongoDB en versió >= 2.4


Agrupació simple[modifica]

Un exemple d'agrupació per manufacturer i que ens farà un recompte dels productes:

db.products.aggregate([
    {$group:
     {
	 _id:"$manufacturer", 
	 num_products:{$sum:1}
     }
    }
])

Una consulta similar en format "compacte":

> db.products.aggregate([{$group: { _id:"$category", num_products:{$sum:1} }} ])

Si només posem el camp que ens interessa (_id) obtindrem un llistat de les diferents "categories" (vindria a ser un DISTINCT de SQL:

> db.products.aggregate([{$group: {_id:"$category"}} ])

Agrupació composta[modifica]

Per seguir diversos criteris d'agrupació només cal que els posem en un array (o diccionari) al _id del $group:

db.products.aggregate([
    {$group:
     {
	 _id: {
	 	"fabricant": "$manufacturer",
	 	"cat": "$category"
	 } ,
	 num_productes:{$sum:1}
     }
    }
])


Aggregation Pipeline (teoria)[modifica]

El "pipeline" és el procés que realitza aggregation per obtenir els resultats. El podem considerar un tub on anem successivament processsant les dades.

Les diferents passes del pipeline son (no necessariament en aquest ordre i es poden fer diversos cops):

  • $project: select (1:1)
  • $match: filtra (n:1)
  • $group (n:1)
  • $sort (1:1)
  • $skip (n:1)
  • $limit (n:1)
  • $unwind: (1:n) genera una entrada per cada element d'array, si existeix un array incrustat en un document.

Per $project i $match podem utiltizar els operadors vistos en les querys simples (find).

Fixeu-vos en què l'única que genera més entrades es la darrera: unwind (les altres o les deixa igual -sort- o bé les redueix). És l'operador més característic d'una DB no-SQL, ja que ens permet desglossar els subdocuments que venen PRE-JOINED (incrustats).

Pipeline query: exemple 1 (blog)[modifica]

Tenim una col·lecció de documents d'un blog, cada document té una estructura com aquesta:

{
	"_id" : ObjectId("50ab0f8bbcf1bfe2536dc3f8"),
	"body" : "Lorem ipsum dolor sit amet,... \n",
	"permalink" : "TqoHkbHyUgLyCKWgPLqm",
	"author" : "machine",
	"title" : "US Constitution",
	"tags" : [
		"trade",
		"fowl",
		"forecast",
		"pest",
		"professor",
		"willow",
		"rise",
		"brace",
		"ink",
		"road"
	],
	"date" : ISODate("2012-11-20T05:05:15.229Z")
	"comments" : [
		{
			"body" : "blablabla...",
			"email" : "[email protected]",
			"author" : "Linnie Weigel"
		},
		{
			"body" : "blebleble...",
			"email" : "[email protected]",
			"author" : "Sadie Jernigan"
		}
        ]
}

Les dades, com sol ser habitual en MongoDB, estan PREJOINED (comentaris dins dels posts).

Observa la següent consulta:

db.posts.aggregate(
    {$project: {
        "comments.author":1
    }},
    {$unwind:"$comments"},
    {$group:
     {
	 _id: {
	     "auth":"$comments.author"
	 },
	 total:{$sum:1}
     }
    },
    {$sort: {
        "total":1
    }}
)

...ens realitza els següents processos:

  1. (project): sel·lecciona només els autors dels comentaris als posts
  2. (unwind): desglossa cadascuna de les entrades (comentaris, que contenen un array d'autors de comentari) en un sol array amb elements
  3. (group) agrupa per autor i calcula el nombre de cops que apareix cadascun
  4. (sort): ordena per ordre creixent d'aparicions

Pipeline query: exemple 2 (zipcodes)[modifica]

Aquesta utiltiza les dades dels codis postals dels USA.

Calcularem la mitjana de les poblacions de California (CA) i New York (NY) (or lògic!) que tinguin més de 25000 habitants.

Les dades estan per districte postal (zipcode), pel que les poblacions estan separades en districtes. Abans que res caldrà agrupar-los per obtenir les dades de població total de cada ciutat.

db.zipcodes.aggregate(
    /* agrupem per sumar la poblacio dels diferents districtes de cada ciutat */
    {$group: {
        _id: {
            "city":"$city",
            "state":"$state"
        },
        total_pop:{$sum:"$pop"}
    }},
    /* filtrem les ciutats amb poblacio>25000 i de CA i NY */
    {$match: {
        total_pop:{$gt:25000},
        $or: [{"_id.state":"CA"},{"_id.state":"NY"}]
    }},
    /* agrupem i calculem mitjana */
    {$group: {
        _id: {},
        mitjana:{$avg:"$total_pop"}
    }}
)

Exemples típics per pipeline queries[modifica]

Per provar exemples de aggregate és més adient utilitzar una BD pre-joined com Fitxer:Alumnes.js


Mostrar un camp (SELECT)[modifica]

Si només volem mostrar un(s) camp(s) determinats ($project):

> db.students.aggregate( {$project:{_id:0,student_id:1,class_id:1}} )

Mitjana de notes d'un estudiant[modifica]

Etapes:

  1. $match: seleccionem estudiant
  2. $unwind: partim el doc en tants com elements té l'array
  3. $group: calculem mitjana
> db.students.aggregate( {$match:{student_id:0,class_id:16}} , {$unwind:"$scores"}, {$group:{_id:"",mitjana:{$avg:"$scores.score"}}} )

Per veure com evoluciona la query, prova afegint etapa per etapa:


Recompte de TOTS els elements[modifica]

Agrupem per "res" (en la última etapa):

> db.col.aggregate( {...} , {...} , {$group:{_id:"",count:{$sum:1}}} )

Prova de calcular la mitjana.

Error "Exceeded memory limit for $group"[modifica]

Sol passar a partir de la v 2.6, ja que no utilitza el disc dur per resoldre les operacions. Cal afegir-hi la opció allowDiskUse=true com en aquest exemple.

Fixa't en què per poder afegir paràmetres ja no podem utilitzar la forma típica del pipeline:

> db.col.aggregate( {...} , {...} , ... )

Caldrà que utilitzem aquesta, amb un array [] per la pipeline i un objecte {} amb els paràmetres (pel nostre cas, allowDiskUse):

> db.col.aggregate( [ {...} , {...} , ... ] , {allowDiskUse:true} )

Fer un "explain" per mostrar metadades (indexes)[modifica]

> db.col.aggregate( [ {...} , {...} , ... ] , {explain:true} )


Exercicis[modifica]

Arxius de dades per proves:


Importació[modifica]

  1. Agafa el link de les dades de països en format JSON i adapta'l convenientment perquè puguem importar-ho a la nostra BBDD mongoDB.
     https://github.com/mledoze/countries/blob/master/countries.json
    • Pistes pel document JSON:
      • Utiltiza un processador de text estil geany que pugui tractar cerques i substitucions amb seqüències d'escapament (Ex.:"\n") i/o expressions regular (regexp).
      • Un document JSON per mongoDB ha de contenir una entrada (diccionari {}) per línia, sense separació per comes entre línies
      • Eliminar tots els salts de línia ("\n") i els tabuladors ("\t")
      • Després introduïr només els salts de línia necessaris, és a dir, quan hi ha canvi de país, amb "},{". Canviarem la coma per un salt de línia.
      • Us han d'entrar uns 250 països.
  2. Importa els diferents arxius disponibles més amunt en diferentes bases de dades, menys la d'imatges i àlbums, que cal posar-los a la mateixa BBDD en diferents col·leccions.

JavaScript[modifica]

Fes un script que ens generi documents amb una estructura com aquesta (mínim 30 articles):

OJU: els articles i el seu preu han de ser coherents!

{
    "compra_id": 18,
    "client_id": 5,
    "article": "pera", // article a triar entre [pera, poma, préssec]
    "quantitat": 1.56, // nombre aleatori entre 0.5 i 3
    "preu": 2.03       // preu en correspondència a l'article triat
}

Inspecciona la comanda mongoexport i exporta les dades en un arxiu .js

Filtratge simple (find)[modifica]

  1. Amb la BBDD de països: respostes aquí
    1. Llista de noms de països.
    2. Nombre total de països.
    3. Nombre total de països que parlen anglès.
    4. Llistat de països que parlen anglès a Europa.
    5. Nombre total de països que parlen anglès fora d'Europa.
    6. Nombre total de països que parlen anglès com a única llengua.
    7. Llista de països que parlen català (amb display tabulat).
    8. Llista de països que parlen espanyol (només mostrar el nom del país i les llengües parlades).
    9. Llista de països que parlen espanyol i anglès.
    10. Llista de països que parlen francès i anglès (mostrar país, llengües i continent).
    11. Llista de països amb una "j" al nom (case insensitive).
      Mireu aquesta referència (regexp) http://docs.mongodb.org/manual/reference/operator/query/regex/#op._S_regex
    12. Llista de països el nom del qual comença per "j" (case insensitive)
    13. Nom nadiu del país que en espanyol (ES) anomenen "Islandia".
    14. Llista de països amb 4 o més llengües oficials.
    15. ...
  2. Amb la BBDD de les qualificacions d'alumnes: Fitxer:Notes.js | respostes aquí
    1. Llistat ascendent de notes
      Resposta: 51.70205387618566
    2. Estudiant (student_id) amb nota més alta d'un examen.
      Pista: llistat descendent de notes.
      student_id=176 (score=99.96723280505422)
    3. Tipus de nota que té la qualificació més alta.
      exam
    4. Nota més baixa en un quiz.
    5. Quantitat de "homeworks" totals.
    6. Quantitat de "homeworks" per l'estudiant student_id=3

Pipeline queries (aggregation framework)[modifica]

Per poder realitzar aquest apartat has hagut de llegir-te exhaustivament l'apartat "aggregation framework".

Mira't els exemples anteriors i pensa que també en tens:


  1. Amb les qualificacions dels alumnes: Fitxer:Alumnes.js | respostes aquí | o aquí (infla)
    1. Llistat de notes d'examens (utiltiza $unwind)
    2. Mitjana de totes les notes
      Resposta = 49.25
    3. Mitjana dels examens
      Resposta = 50.83
    4. Mitjana de totes les notes de la classe_id = 5
      Resposta = 54.16
    5. Mitjana dels examens de la classe_id = 3
      Resposta = 49.09
    6. Alumne amb millor mitjana (llista de mitjana de notes per alumne ordenada)
      Resposta 1 (suposant alumnes diferents a cada classe) : alumne 3 de la classe 11, mitjana = 86.87
      Resposta 2 (suposant alumne_id com a alumne únic) : alumne 44, mitjana = 65.57
    7. Classe amb pitjor mitjana (llista de mitjana de notes per classe ordenada descendent)
      Resposta = classe 2 amb mitjana 42.59
    8. Llista ordenada de mitjana d'examens per classe amb recompte del nº d'examens per classe.
      Millor i pitjor resultat?
      Millor = classe 20 amb 70.87 (7 examens)
  2. Amb Fitxer:Countries2014.json.txt la BBDD de països 2014 | respostes aquí | o aquí (infla)
    (ull, la de la web ja no ens serveix perquè l'estructura actual de les dades no ens permet certs càlculs)
    1. Llista de continents i la seva població.
    2. Llista dels països que no tenen continent (="") ordenada per població.
    3. Nombre de països amb anglès com a única llengua oficial.
    4. Nombre de països amb francès com llengua oficial (poden tenir altres).
    5. Llista ordenada de països segons el nombre de llengües oficials.
    6. Llista de països amb 4 o més llengües oficials.
    7. Quin és el país amb més llengües oficials?
    8. Recompte de països segons el seu nombre de llengües oficials.
    9. Llista de llengües (language o languageCodes) ordenades per la quantitat de països que les tenen com a oficials.
    10. Llista de llengües (language o languageCodes) ordenades pel total de gent que la parla (oficialment).
    11. Llista "top ten" de països amb major nombre de fronteres.
    12. Llista de monedes amb el nombre de països que les parlen. Ordena-la descendentment.
    13. Llista de monedes amb el llistat de continents on s'utilitzen. Ordena-la descendentment.
    14. Llista de contintents amb el nombre de països que el formen. Ordena-la descendentment.
    15. Llista de contintents amb el nombre de llengües que es parlen. Ordena-la descendentment.
    16. Llista de subregions ordenades pel nombre de països que tenen. Ordena-la descendentment.
    17. Llista de subregions ordenades pel nombre d'habitants. Ordena-la descendentment.
    18. ...
  3. Amb els zips (codis postals dels USA):
    1. Removing Rural Residents: In this problem you will calculate the number of people who live in a zip code in the US where the city starts with a digit. We will take that to mean they don't really live in a city.
      Resposta: 298015
    2. ...
  4. Amb els posts del blog: Blog posts
    1. ...
    2. ...
  5. Amb les imatges i àlbums: images.json / Fitxer:Albums.json
    1. Esborrar les imatges que no estan referenciades en cap àlbum (cal fer un script en JS).
    2. ...