MongoDB

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

Intro No-SQL

Algunes característiques:

  • Treballem amb 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.


Primeres passes

  • Monogo shell:
    $ mongo
  • Seleccionar una db (o crear):
    > use <nom_db>



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++) {
   db.coses.save( {
      "num":i,
      "cosa": ["tablet","smartphone","xocolata"][Math.floor(Math.random()*3)]
   })
}

Un altre exemple:

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


CRUD

Create Read Update Delete


Create / Insert / Save

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

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


Read / Select / Find

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]

Altres operadors: http://docs.mongodb.org/manual/reference/operator/query/

TODO: Unir dues queries amb $or...

Ordenar resultats (sort): http://docs.mongodb.org/manual/reference/method/cursor.sort/


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

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.


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ó

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

Filtratge simple (find)

  1. Amb la BBDD de països:
    1. Llista de noms de països.
    2. Nombre total de països.
    3. Nombre total de països que parlen anglès.
    4. Llista de països que parlen català (amb display tabulat).
    5. Llista de països que parlen espanyol (només mostrar el nom del país i les llengües parlades).
    6. ...

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 un tutorial utilitzant els codis postals dels USA.

  1. Amb els països:
    1. Llista de continents i la seva població.
    2. Llista dels països que no tenen continent (="")
    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. ...
  2. Amb els productes:
    1. ...
  3. Amb els zips (codis postals):
    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 les qualificacions dels alumnes:
    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)