MongoDB
Contingut
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
- Instal·lar darrera versió (2.4.8) a Ubuntu:
- Monogo shell:
$ mongo
- Seleccionar una db (o crear):
> use <nom_db>
- Exemple amb dades:
- Importar-lo:
$ mongoimport -d students -c grades < grades.ef42a2b3e7ff.js
- ...
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
- En Mongo Shell: http://docs.mongodb.org/manual/reference/crud/
- En PyMongo: http://api.mongodb.org/python/current/api/pymongo/collection.html
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})
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:
- Mongo Shell Update: http://docs.mongodb.org/manual/tutorial/modify-documents/
- Pymongo Update: http://api.mongodb.org/python/current/api/pymongo/collection.html#pymongo.collection.Collection.update
- Update operators: http://docs.mongodb.org/manual/reference/operator/update/
La operació de Update es pot resoldre de 2 maneres:
- 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}} )
- Exemple en mongo shell:
- db.<col>.save( <doc> ) ...on <doc> ha de ser el nou document modificat amb el "_id" (PK) pertinent
Delete / Remove
Referències:
- Pymongo remove: http://api.mongodb.org/python/current/api/pymongo/collection.html#pymongo.collection.Collection.remove
- ...
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.
- http://docs.mongodb.org/manual/aggregation/
- http://docs.mongodb.org/manual/reference/operator/aggregation/
- Exemples: Fitxer:Products.js
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:
- Vídeo: http://www.youtube.com/watch?v=WazN2DS8s8c&list=PLOqwUgCzNJcsC1ARX93IZZLf9k3rVRrCn
- Doc: http://docs.mongodb.org/manual/core/aggregation-pipeline/
- Exemples: http://docs.mongodb.org/manual/tutorial/aggregation-zip-code-data-set/
- Operadors: http://docs.mongodb.org/manual/reference/operator/aggregation/
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.
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:
- (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 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:
Pipeline queries (aggregation framework)
Per poder realitzar aquest apartat has hagut de llegir-te exhaustivament l'apartat "aggregation framework".
- Amb els zips (codis postals):
- 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
- ...
- 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.
- Amb els productes:
- ...
- Amb les qualificacions dels alumnes:
- 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 = alumne 3 de la classe 11, mitjana = 86.87
- 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)