MongoDB

De Cacauet Wiki
Salta a la navegació Salta a la cerca

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

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

  • 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

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

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;
}



CRUD

Create Read Update Delete


Create / Insert / Save

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

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


Read / Select / Find

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}} )


Update

Referències:

La operació de Update es pot resoldre de 2 maneres:

  1. 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}} )
  2. db.<col>.save( <doc> ) ...on <doc> ha de ser el nou document modificat amb el "_id" (PK) pertinent


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:


Indexes

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

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

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

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

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.

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

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

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

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

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

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)

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)

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)

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"}
    }}
)



Exercicis

Arxius de dades per proves:


Importació

  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

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)

  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
    2. Estudiant (student_id) amb nota més alta d'un examen.
      Pista: llistat descendent de notes.
    3. Tipus de nota que té la qualificació més alta.
    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)

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í
    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 = alumne 3 de la classe 11, mitjana = 86.87
    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 la Fitxer:Countries2014.json.txt BBDD de països 2014 (ull, la de la web ja no ens serveix perquè l'estructura actual de les dades no ens permet certs càlculs):respostes aquí
    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 (languagesCodes) ordenades per la quantitat de països que les tenen com a oficials.
    10. Llista de llengües (languagesCodes) ordenades pel total de gent que la parla (oficialment).
    11. Llista "top ten" de països amb major nombre de fronteres.
    12. ...
  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. ...