Ahora que ya hemos aprendido a diseñar correctamente la estructura de los datos en MongoDB, y que sabemos realizar todo tipo de consultas, seguro que se nos ocurren multitud de posibilidades con documentos anidados y estructuras complejas para que nuestra aplicación sea realmente rápida.

Sin embargo, cuando tengamos una cantidad masiva de datos, por defecto MongoDB solo nos ofrecerá un alto rendimiento cuando las consultas utilicen filtros que utilicen el identificador (_id) del documento raiz, ya que de otro modo MongoDB deberá buscar sobre todos los elementos de la colección antes de empezar a procesar los resultados. En la publicación de hoy vamos a aprender a crear otros indices que nos permitan realizar operaciones eficientes sobre otros atributos, así como algunos detalles sobre su uso.

Anteriormente

Guía 5: Esquema de datos dirigido por la aplicación

Trabajar con índices en MongoDB

Tipos de índice

Indices simples (single field index)

Los índices simples nos permiten definir un índice sobre un único campo. El orden (ascendente o descendente) en este caso, no es relevante, ya que mongoDB puede recorrerlo en cualquier dirección.

> db.scores.ensureIndex({ score: 1 });

Indices compuestos (compound index)

Un caso más interesante es el indice compuesto, que nos permite establecer índices sobre varios atributos en conjunto. Por ejemplo:

> db.scores.ensureIndex({ score: 1, username: -1 })

En este caso, el orden de los atributos cuando se crea el índice es relevante, ya que el índice ordenará primero por el primero y después por el segundo. Es decir, en el ejemplo anterior el indice ordena por puntuación, y por cada valor de puntuación, ordenará por nombre de usuario.

El orden de ordenacion (ascendente o descendente) tambien es importante en este caso, ya que puede determinar cuando el índice se usará en una operación de ordenación.

Indices multiclave (multikey index)

Este tipo de indice permite indexar el contenido almacenado en los arrays. Si indexas un atributo que contiene un array, MongoDB creará automaticamente entradas separadas para cada elemento del array en la tabla de indices, y hace muy eficiente la búsqueda por valores de los arrays.

> db.scores.ensureIndex({ favorites: 1 })

No se pueden crear índices multiclave sobre dos campos de tipo array en el mismo índice.

Indices geoespaciales (geospatial index)

Para soportar consultas geoespaciales eficientes sobre datos de tipo coordenada, MongoDB soporta dos indices especiales: 2d indexes para geometria de planos y 2sphere indexes para geometria esférica.

Para poder utilizar estos indices, el atributo debe ser un objeto geolocalizado según la especificación GeoJson (en este caso un punto con las coordenadas como un array de Longitud y Latitud).

> db.places.insert({ _id : 1, name: “mi casa”, loc: { type : “Point”, “coordinates” : [ -37.478, 4.4884 ] } })
> db.scores.ensureIndex({ loc: “2dsphere” })

Ahora podríamos utilizar operadores de búsqueda, por ejemplo, por cercanía, especificando la distancia máxima en metros alrededor de un punto concreto con la misma notación:

> db.places.find({ loc: {
$near : {
$geometry : { type: “Point”, coordinates : [ -130, 39 ] },
$maxDistance : 10000
} } });

Indices de texto (text indexes)

MongoDB proporciona un tipo de indice que soporta la busca de texto en una colección. Estos índices no almacenan las “stop words” específicas de un lenguaje (por ejemplo, “de”, “la”, “o”) y simplifica (stem) las palabras para almacenar solo su forma “raiz”.

> db.libros.ensureIndex({ titulo: “text” })
> db.libros.find( { $text: { $search : “dog” } })

Podemos definir una proyección para obtener la relevancia del resultado en función del número de coincidencias que el índice de texto completo haya encontrado (ya que las búsquedas de este tipo no son filtros en el sentido estricto).

> db.libros.find( { $text: { $search: “dog” } }, { score : { $meta: “textScore” }} )

Hashed Indexes

Se trata un tipo de índice especial para soportar sharding hash.

Propiedades de los índices

Indices únicos

Una propiedad unique para un indice hace que MongoDB rechace valores duplicados para ese atributo indexado dentro de una colección. Además de esta restricción, funcionan como cualquier otro índice.

> db.scores.ensureIndex({ username: 1, score: -1 }, { unique: 1 })

Si quisiéramos eliminar los duplicados durante el proceso (ojo que puede resultar peligroso ya que implica perdida de datos):

> db.scores.ensureIndex({ username: 1, score: -1 }, { unique: true, dropDups: true })

Indices dispersos (sparse indexes)

Una propiedad sparse en un indice hace que el índice solo contenga entradas para documentos que tienen el campo indexado (si el documento no tiene el campo que estamos indexando, no estará indexado).

Es posible combinar sparse con unique para rechazar documentos que tengan valores duplicados para un campo pero ignore los documentos que no tienen ese atributo.

> db.scores.ensureIndex({ username: 1, score: -1 }, { unique: 1, sparse: true })

Indices TTL (TTL indexes)

Son indices especiales de MongoDB que permiten eliminar documentos automaticamente de una colección despues de una cantidad de tiempo concreta. Esto es ideal para ciertos tipos de información como datos generados automaticamente, logs, sesiones…

Uso de los indices

Consultas cubiertas

Cuando los criterios de filtro de una consulta y la proyección de los datos incluyen solo atributos indexados, MongoDB devolverá directamente los resultados desde el índice sin escanear ningún documento o acceder a la memoria para su lectura. A este tipo de consulta se la denomina Covered Query y puede ser realmente eficiente.

Ejemplo:

> db.scores.ensureIndex({ score: 1 })
> db.scores.find({ score: { “$lt”: 30 } }, { score : 1, _id : 0 })

Creamos un índice sobre el atributo score. Después consultamos los documentos utilizando únicamente los atributos indexados en el filtro, y devolvemos únicamente los datos indexados empleando una proyección (la proyección es el segundo parametro de la operación find y determina qué campos queremos obtener como respuesta). Esta operación se considera una operación cubierta por el índice y será muy eficiente.

Intersección de índices (index intersection)

MongoDb puede emplear distintos índices para completar una operación. Si un índice puede emplearse para una parte de la condición de la query y otro índice puede completar otra parte de la condición, entonces MongoDB usará la intersección de ambos para obtener el resultado.

Indices y sharding

Como veremos en el capitulo de sharding, MongoDB puede estar distribuido en varias máquinas, donde cada una de ellas almacena una parte de la totalidad de los datos. Para localizar un documento, MongoDB debe enviar la consulta en cada nodo, por lo que si no se sabe en cual está el documento, la consulta se realizará en todos ellos (broadcast query).

Para evitar este comportamiento, se define una clave especial (clave de shard) que actuará como índice de nodos, y que permite determinar en que nodo está un documento concreto, ya que los datos están particionados siguiendo una lógica. Para aprovechar esta clave de shard, la clave debe estar incluida en todas las consultas que hagamos.

Operaciones

Crear índices

Para crear un índice, emplearemos la operación ensureIndex especificando el criterio del índice tal y como se ha descrito antes

> db.scores.ensureIndex({ username: 1, score: -1 });

Esta operación es bloqueante (a nivel de base de datos), si queremos evitarlo hay que usar la opción “background: true” que lo creará en segundo plano, aunque existe un límite de creación de un único índice cada simultáneamente.

Listar los índices existentes en una colección

> db.scores.getIndexes()

Eliminar un índice existente

> db.scores.dropIndex({ username: 1, score: -1 })

Forzar el uso de un índice en una operación

Si por alguna razón MongoDB no utilizaría un índice cuyo uso deseamos forzar, podemos utilizar el siguiente comando, especificando el índice que queremos utilizar:

> db.scores.find({ username: “findemor”}).hint({ username: 1, score: -1 })

Hay que tener en cuenta que los documentos que no existan en el índice, no serán listados, y que el orden del índice es muy importante, ya que hay que intentar quitar cuanto antes el mayor volumen de datos (es decir, los primero atributos del índice deberían ser los más segregados de la base de datos).

Forzar que no se use ningún índice

> db.scores.find({ username: “findemor” }).hint({ $natural: 1 })

Analizar el uso de los índices

Podemos utilizar algunas operaciones para comprender cómo se procesará la consulta y analizar el uso de los índices existentes.

Obtener una explicación de cómo se procesará la consulta

Basta con aplicar la operación explain a la consulta que se desee. Esta operación ofrece muchísima información acerca de qué indices se utlizaron y como, cuantos elementos se recorrieron en la búsqueda, etc. Para conocer los detalles es recomendable leer la documentación oficial.

> db.places.find({ _id : 1 }, { _id : 1 }).explain()

"cursor": "IDCursor",  
"n": 1,  
"nscannedObjects": 0,  
"nscanned": 1,  
"indexOnly": true,  
"millis": 0,  
"indexBounds": {  
"_id": [  
[  
1,  
1  
]  
]  
},  
"server": "MacBook-de-Findemor.local:27017"  
}

En el ejemplo podemos ver que, al hacer una consulta Cubierta (es decir, la proyección y el filtro están en el índice, por lo que no hay que leer documentos del disco ni de la memoria), hemos obtenido 1 resultado (n), no se escaneó ningun documento en la colección (nscannedObjects), se escaneó solo un documento en el índice (nscanned), la consulta estaba cubierta (indexOnly), tardó 0 millisegundos…

Hay que tener en cuenta que los datos obtenidos pueden diferir en funcion de la versión de mongo que tengais instalada (la mia es un poco antigua).

Obtener información acerca de una colección

Con la operación stats podemos conocer el numero de indices, el namespace, cantidad de documentos que contiene, tamaño de los indices…

> db.places.stats()

"ns": "test.places",  
"count": 2,  
"size": 224,  
"avgObjSize": 112,  
"storageSize": 8192,  
"numExtents": 1,  
"nindexes": 1,  
"lastExtentSize": 8192,  
"paddingFactor": 1,  
"systemFlags": 1,  
"userFlags": 1,  
"totalIndexSize": 8176,  
"indexSizes": {  
"_id_": 8176  
},  
"ok": 1  
}

Latencias, Autolog y Profiling

Para terminar con esta publicación, vamos a ver como detectar malos rendimientos en las consultas y así poder ver que índices serían recomendables en nuestra base de datos.

En MongoDB se registra automaticamente toda consulta que tome más de 100ms. Si queremos que estos resultados se escriban en la colección system.profile (además de mostrarse por consola, que es el comportamiento por defecto), tenemos que activar el profiler, que tiene tres modos de funcionamiento:

  • 0 : el profiler no registrará nada
  • 1 : el profiler registrará las operaciones lentas
  • 2 : el profiler registrará todas las operaciones
  • Para ello, tenemos que añadir las siguientes opciones al arrancar la instancia de mongo

    -profile 1 -slowms 20

    O una vez arrancado, podemos obtener o establecer nuevos valores

    > db.getProfilingLevel()
    > db.getProfilingStatus()
    > db.setProfilingStatus(1,20)

    Además existen varias utilidades para facilitarnos el diagnóstico de la base de datos

  • mongotop es un programa que nos permite qué colecciones están gastando más tiempo
  • mongostat devuelve principalmente el número de índices que se pierden… aunque resulta dificil de comprender en ocasiones (por ejemplo, un valor del 0% puede ser perfecto o puede significar que no tengas ningún índice).
  • Enlaces a la guía completa