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

De Cacauet Wiki
Salta a la navegació Salta a la cerca
Línia 16: Línia 16:
 
**: 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 24:
 
* 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

Revisió del 21:17, 2 gen 2014

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

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:

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

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: