Hace ya unos días os hablaba de NoSQL y su conveniencia para ciertos proyectos. Hoy os voy a hablar de las características de un sistema de base de datos NoSQL en concreto, el cual he tenido ocasión de probar a fondo en un par de proyectos, y que sin duda se ha convertido en uno de mis favoritos: MongoDB.
Descripción y licencia
MongoDB es un sistema de base de datos multiplataforma orientado a documentos, de esquema libre. Como ya os expliqué, esto significa que cada entrada o registro puede tener un esquema de datos diferente, con atributos o “columnas” que no tienen por qué repetirse de un registro a otro. Está escrito en C++, lo que le confiere cierta cercanía al bare metal, o recursos de hardware de la máquina, de modo que es bastante rápido a la hora de ejecutar sus tareas. Además, está licenciado como GNU AGPL 3.0, de modo que se trata de un software de licencia libre. Funciona en sistemas operativos Windows, Linux, OS X y Solaris.
Las características que más destacaría de MongoDB son su velocidad y su rico pero sencillo sistema de consulta de los contenidos de la base de datos. Se podría decir que alcanza un balance perfecto entre rendimiento y funcionalidad, incorporando muchos de los tipos de consulta que utilizaríamos en nuestro sistema relacional preferido, pero sin sacrificar en rendimiento.
Terminología básica en MongoDB
En MongoDB, cada registro o conjunto de datos se denomina documento. Los documentos se pueden agrupar en colecciones, las cuales se podría decir que son el equivalente a las tablas en una base de datos relacional (sólo que las colecciones pueden almacenar documentos con muy diferentes formatos, en lugar de estar sometidos a un esquema fijo). Se pueden crear índices para algunos atributos de los documentos, de modo que MongoDB mantendrá una estructura interna eficiente para el acceso a la información por los contenidos de estos atributos.
Formato de los documentos e ideas para la organización de datos
Los distintos documentos se almacenan en formato BSON, o Binary JSON, que es una versión modificada de JSON que permite búsquedas rápidas de datos. Para hacernos una idea, BSON guarda de forma explícita las longitudes de los campos, los índices de los arrays, y demás información útil para el escaneo de datos. Es por esto que, en algunos casos, el mismo documento en BSON ocupa un poco más de espacio de lo que ocuparía de estar almacenado directamente en formato JSON. Pero una de las ideas claves en los sistemas NoSQL es que el almacenamiento es barato, y es mejor aprovecharlo si así se introduce un considerable incremento en la velocidad de localización de información dentro de un documento.
Sin embargo, en la práctica, nunca veremos el formato en que verdaderamente se almacenan los datos, y trabajaremos siempre sobre un documento en JSON tanto al almacenar como al consultar información. Un ejemplo de un documento en MongoDB podría ser perfectamente éste:
{ “_id” : “4da2c0e2e999fb56bf000002” “title” : “Una introducción a MongoDB”, “body” : “Lorem ipsum dolor sit amet…”, “published_at” : “2011-05-09T18:17:07-07:00”, “author_info” : { “_id” : “4dc8919331c0c00001000002” “name” : “Carlos Paramio” }, “tags” : [“MongoDB”, “NoSQL”, “Bases de datos”] “comments” : [ { “author_info” : { “name” : “Jorge Rubira”, “email” : “email1@example.com” }, “body” : “Test”, “created_at” : “2011-05-10T10:14:01-07:00” }, { “author_info” : { “name” : “Txema Rodríguez”, “email” : “email2@example.com” }, “body” : “Otro test”, “created_at” : “2011-05-10T10:14:09-07:00” } ] “liked_by” : [“4d7cf768e999fb67c0000001”, “4da34c62ba875a19d4000001”] }
Como podemos adivinar, este documento pretende representar la manera en que podrían almacenarse los datos correspondientes a un post de un blog. Los atributos “_id” (o clave principal) pueden tener el formato que se desee, aunque MongoDB utiliza un valor parecido a un UUID en hexadecimal por defecto si no se ha especificado ninguno. A pesar de parecer un valor completamente aleatorio (aunque ya sabemos que la aleatoriedad real no existe en informática), utilizan como base una semilla basada en la MAC de la interfaz de red de la máquina (y otros detalles de la misma) para evitar que dos máquinas diferentes puedan generar el mismo valor para la clave de un documento. Y los primeros bytes corresponden a una marca de tiempo, de modo que las claves se ordenan de forma natural por orden de creación (o casi, pues está claro que las distintas máquinas corriendo MongoDB deben tener la fecha y hora sincronizadas) sin tener que mirar cuál fue el último valor usado. Una solución inteligente, a mi modo de ver bastante más eficiente que un campo autonumérico, en especial para evitar que una máquina bloquee la inserción de registros en una colección sólo para asegurarse que no se dan condiciones de carrera al intentar dos máquinas diferentes escribir un documento con el mismo valor para “_id”. Por cierto, este atributo “_id” es el único obligatorio para un documento.
Las etiquetas y los comentarios están en el propio documento que representa al post, en lugar de guardarlos en colecciones separadas y utilizar claves foráneas para referenciar a los mismos. Sin embargo, en el atributo “liked_by” sí que guardamos una relación de claves, que corresponden a los usuarios que han marcado el post como que les ha gustado. Utilizar una forma u otra dependerá de las necesidades de acceso a estos datos En este caso, por ejemplo, sabemos que no vamos a pintar información sobre los usuarios que han marcado un post con un “me gusta”, pero sí queremos ver cuántos lo han marcado así, o si el usuario actual ya lo ha marcado o no, con lo que almacenar únicamente las claves de esos usuarios y guardar su información personal detallada en otra colección es lo más conveniente.
Por supuesto, no es necesario pedir a MongoDB que nos devuelva todo el documento cada vez que lo consultamos. Si por ejemplo vamos a pintar únicamente un listado de posts recientes, seguramente sea suficiente obtener el atributo “title”, con los documentos ordenados por “published_at”. Así, ahorramos ancho de banda entre el motor de base de datos y la aplicación, al mismo tiempo que memoria dado que no hay que instanciar todo el documento. Además, si tenemos muchos miles de visitantes, el atributo “liked_by” podría llegar a crecer bastante. Aunque el tamaño de un documento de MongoDB puede llegar hasta los 16 Megabytes, con lo que podemos almacenar bastante información dentro de un único documento sin necesidad de utilizar referencias, si así lo necesitamos. En caso de que tuviéramos que almacenar mucho más, habría que optar por utilizar otro esquema.
A veces, toca desnormalizar para poder tener a mano la información necesaria a la hora de pintar un post. Es por eso que en el atributo “author_info” he utilizado una versión intermedia: Si bien tenemos la clave principal del usuario que ha escrito este post, como es habitual que pintemos el nombre del autor, he almacenado también dicho nombre en el documento que representa al post, para que no sea necesario realizar una segunda consulta a la colección “usuarios”. Estas desnormalizaciones dependen nuevamente del uso que se den a los datos. En este caso, tengo claro que el nombre de un usuario no va a cambiar demasiado, así que recorrer todos los posts para cambiar este valor en caso de que el usuario realice esta operación, si bien es una modificación que podría llevar un tiempo considerable para ejecutarse, no es una operación habitual frente a la consulta del nombre del autor, y por tanto compensa. Incluso podríamos llegar a tratar el post como algo más permanente, de modo que aunque un usuario cambiara su nombre a posteriori, el nombre utilizado para firmar los posts anteriores no varíe, o sencillamente los posts puedan firmarse con diferentes pseudónimos.
Como creo que habrá quedado patente a estas alturas, el modelado del esquema de datos con MongoDB depende más de la forma en que consultaremos o actualizaremos los datos que de las limitaciones del propio sistema.
Cómo consultar los datos
Sin entrar demasiado en detalles acerca de todas las posibilidades que MongoDB nos ofrece para consultar los datos almacenados, sí quisiera nombrar algunas de ellas, para hacer notar que no estamos frente a un sistema simple de almacenamiento de pares clave-valor.
En primer lugar, MongoDB nos permite utilizar funciones Map y Reduce escritas en Javascript para seleccionar los atributos que nos interesan de los datos, y agregarlos (unificarlos, simplificarlos) en la manera deseada, respectivamente. Esto es algo habitual en muchos sistemas NoSQL, y en algunos casos es incluso la única forma posible de consultar datos. Claro está que muchas veces necesitamos algo bastante más sencillo que ésto.
En MongoDB se pueden utilizar consultas al valor de un atributo específico. Por ejemplo, podemos capturar el post que tiene un determinado título:
db.posts.find({‘title’ : ‘Una introducción a MongoDB’})
El valor a consultar puede estar anidado en un tipo de datos más completo en el atributo del documento (por ejemplo, como valor de un hash asociado al atributo, o como el valor de uno de los ítems de un array). Se utiliza un punto como separador de los nombres de las claves de los diferentes hashes que hay que recorrer hasta llegar al valor deseado. Por ejemplo, la siguiente consulta devolvería todos los posts escritos por un determinado autor:
db.posts.find({‘author_info._id’ : ‘4da2c0e2e999fb56bf000002’})
Y esta otra los posts etiquetados con MongoDB:
db.posts.find({‘tags’ : ‘MongoDB’})
El hash utilizado como conjunto de condiciones que deben cumplir los documentos a devolver puede incluir operadores de muy diversos tipos, no sólo comparadores del valor absoluto buscado. Algunos de ellos son:
-
$all : Para indicar que el array almacenado como valor del atributo debe tener los mismos elementos que el proporcionado en la condición.
-
$exists : Para comprobar que el atributo existe en el documento.
-
$mod : Para comprobar el resto de una división del valor del atributo por un número.
-
$ne : Para indicar que el valor no puede ser el proporcionado.
-
$in : Para indicar que el valor debe estar entre alguno de los proporcionados.
-
$nin : Contrario de $in.
-
$or : Para indicar que se debe cumplir al menos una condición de entre un grupo de condiciones.
-
$nor : Contrario de $or.
-
$size : Para indicar el número de elementos que debe haber en el array almacenado como valor.
-
$type : Para comprobar el tipo del valor almacenado (número, cadena…)
-
Expresiones regulares : El valor debe concordar con la expresión regular indicada.
Y muchos, muchos más. Los resultados se pueden agrupar, ordenar, contar, paginar, y otras tantas operaciones comunes sin necesidad de recurrir al farragoso Map / Reduce. Y siempre que los atributos consultados tengan definidos un índice, la velocidad de las consultas es espectacular.
Bueno, por ahora creo que es suficiente como para que sirva de introducción a este fabuloso motor de base de datos. En futuros artículos procuraré hablaros de sus otras características a la hora de escalarlo para su uso en múltiples máquinas, así como otros mecanismos interesantes de MongoDB como los índices geoespaciales. Si queréis experimentar con él, y trabajáis habitualmente con Ruby, tal vez os interese echar un vistazo a un Object Document Mapper para este lenguaje que escribí para un proyecto, y que facilita la interacción con el motor desde tus modelos: MongoODM.