Indexar en MongoDB
¿Cómo funciona?
En terminología de bases de datos, relacionales o no, un índice es muy parecido al que se encuentra al final de cualquier libro: agrupa los términos importantes que aparecen en el libro con, enfrente, los números de página en los que se encuentran. Esto simplemente nos ahorra tener que releer todo el libro cuando buscamos un solo término.
Por analogía, un índice colocado en el campo o subcampo de una colección significa que no tenemos que recorrer toda nuestra colección para encontrar los valores de este campo (o subcampo) correspondientes a nuestra consulta y, por lo tanto, ayuda a mantener el tiempo de ejecución de nuestras consultas lo más corto posible.
Los índices presentan ventajas e inconvenientes: mejoran considerablemente los tiempos de ejecución de las solicitudes de lectura, pero ralentizan las operaciones de escritura, como las inserciones, supresiones o actualizaciones, que requieren su reconstrucción. Sin embargo, las ralentizaciones observadas son generalmente insignificantes en comparación con la reducción del tiempo de ejecución que generan.
Para saber qué campos de una colección deben indexarse, hay que tener una idea de las consultas que se hacen sobre ella y de su frecuencia. Los campos a los que se dirigen con frecuencia las consultas deben indexarse prioritariamente. Un sitio...
Índices simples
Cuando se crea una colección, MongoDB genera automáticamente un índice sobre el campo _id. Este índice no puede borrarse, ya que garantiza la propia unicidad de este identificador. Para crear un índice, se debe utilizar la función createIndex, cuya sintaxis es la siguiente:
db.collection.createIndex(< campo_y_tipo >, < opciones >)
Le presentamos la nueva versión de nuestra colección personas:
db.personas.drop()
db.personas.insertMany(
[
{"apellido": "Durand", "nombre": "René", "intereses": ["jardinería",
"bricolaje"], "edad": 77},
{"apellido": "Durand", "nombre": "Gisèle", "intereses": ["bridge",
"cocina"], "edad": 75},
{"apellido": "Dupont", "nombre": "Gaston", "intereses": ["jardinería",
"petanca"], "edad": 79},
{"apellido": "Dupont", "nombre": "Catherine", "intereses": ["cocina"], "edad": 66},
{"apellido": "Duport", "nombre": "Eric", "intereses": ["cocina",
"petanca"]...
Índices compuestos
Un índice puede referirse a más de un campo: es lo que se conoce como índicecompuesto (compound index). En este tipo de índice, el orden en que se enumeran los campos es importante. Eliminemos nuestro índice idx_edad y creemos un índice compuesto llamado idx_edad_nombre que buscará la edad y luego el nombre de las personas:
db.personas.createIndex({"edad": 1, "nombre": 1},
{"name": "idx_edad_nombre"})
El índice se ordenará primero por valores de edad crecientes y después por orden alfabético del nombre dentro de cada uno de los diferentes valores de edad.
Cuando se utiliza con intercalación un índice compuesto cuyo prefijo no es una cadena de caracteres, una matriz o un subdocumento, una consulta que utilice una intercalación incorrecta para el campo de texto indexado puede seguir basándose en el prefijo del índice.
Supongamos que nuestro índice anterior se creó de esta forma:
db.personas.createIndex(
{ "edad": 1, "nombre": 1},
{ "nombre": "idx_edad_nombre", "intercalacion": { local: "es" }}
)
La siguiente consulta, que utiliza la intercalación binaria básica para comparar cadenas de caracteres (y no el idioma es), podrá sin embargo utilizar idx_nombre_edad porque edad es el prefijo:
db.personas.find({"edad": {$gt: 40}, "nombre": "Christophe"})
Prefijo del índice
Una consulta puede utilizar todos los campos que componen el índice compuesto, o solo una subsección, siempre que esté formada por campos que aparezcan al principio del índice. Esta subsección...
Índices únicos
Los índices únicos garantizan que un valor determinado aparecerá como máximo una vez en el índice. Antes se vió que se colocaba sistemáticamente un índice de este tipo en el campo _id de los documentos de una colección, ¡y que era imposible borrarlo! En el estado actual de nuestra colección de personas, no podríamos poner este tipo de índice en el campo apellido porque la mayoría de los valores de este campo tienen varias apariciones. En cambio, podríamos crear un índice único para el campo nombre, donde todos los valores son únicos:
db.personas.createIndex({"nombre": 1}, {"unique": true})
A partir de ahora, cualquier intento de insertar a una persona con un nombre que ya esté presente en uno de los documentos de nuestra colección resultará en un fracaso estrepitoso. Evidentemente, colocar un índice único en un campo que muy probablemente contenga duplicados es una decisión bastante desacertada.
Además, ahora que nuestro campo apellido está sujeto a una restricción de unicidad, no podremos insertar más de un documento que no contenga este campo. La siguiente inserción falla en parte porque, como apellido se marcó como null al insertar el primer documento, no es posible insertar un segundo documento...
Indexar objetos y tablas
La indexación de documentos contenidos en otros documentos se realiza de forma muy similar a la de un campo escalar. Supongamos que algunos de los documentos de nuestra colección de personas contienen ahora un subdocumento bajo la clave dirección que contiene detalles de la dirección postal de un individuo. Para ilustrarlo, actualicemos la información sobre el Sr. y la Sra. Lejeune:
db.personas.updateMany(
{"apellido": "Lejeune"},
{$set : {
"dirección": {
"número": 546
"vía": "calle",
"nombre": "Descartes",
"cp": 71230,
"ciudad": "Saint-Vallier"
}
}
})
A continuación, vamos a crear un índice basado en el campo cp del documento anidado en la clave dirección:
db.personas.createIndex({"dirección.cp": 1}, {"name": "idx_direccion_cp"})
Esta consulta puede basarse en dicho índice:
db.personas.find({"dirección.cp"...
Índices geoespaciales
El uso de la geolocalización está completamente arraigado en los internautas: la ruta al destino de vacaciones, el patinete eléctrico más cercano, el tiempo medio que se tarda en llegar andando al restaurante favorito... Es difícil de evitar.
MongoDB ofrece varios tipos de índices para funcionar con consultas geoespaciales: los índices 2dsphere se utilizan para consultas geoespaciales sobre una superficie esférica, mientras que los índices 2d se utilizan para consultas sobre un plano euclidiano. Si el campo que contiene los datos geoespaciales en la colección plan se llama geodata, se crea un índice 2d ejecutando el siguiente comando:
db.plan.createIndex({"geodata": "2d"})
Un índice de tipo 2dsphere sobre el campo geodata de una colección denominada sphere se plantea de la siguiente manera:
db.sphere.createIndex({"geodata": "2dsphere"})
1. Índices 2d
Los índices 2d utilizan los pares de coordenadas llamados heredados (legacy), es decir, se mantienen por razones de compatibilidad con versiones anteriores. Este tipo de coordenadas se utilizó hasta la versión 2.2 de MongoDB, pero desde entonces ha sido sustituido por GeoJSON, un formato unificado para describir datos geográficos utilizando JSON. Este formato está en proceso de estandarización; sus especificaciones completas pueden consultarse en https://tools.ietf.org/html/rfc7946.
Si se utilizan estas coordenadas heredadas (legacy), es aconsejable almacenar las coordenadas de un punto en forma de tabla, empezando siempre por la abscisa, seguida de la ordenada:
db.plan.insertOne({"apellido": "Point 1", "geodata": [1,1]})
Si estas coordenadas legacy representan un par longitud/latitud, la longitud debe aparecer en primer lugar:
db.plan.insertOne({"apellido": "Point 1", "geodata": [4.805528, 43.949317]})
Sin embargo, también es posible almacenarlos en forma de subdocumento, con la única condición de que la longitud (anotada aquí como lon) aparezca en primer lugar:
db.plan.insertOne({
"apellido": "Point 2
"geodata": {"lon": 4.805528, "lat": 43.949317}
})...
Índices parciales
Los índices parciales se introdujeron en la versión 3.2 para sustituir gradualmente a los índices sparse (disperso), mucho más limitados. Un índice sparse solo contiene documentos en los que está presente el campo objetivo.
Un índice parcial se comporta de forma similar a un índice sparse en el sentido de que también puede indexar un subconjunto de los documentos contenidos en una colección. Estos índices utilizan un filtro que actúa como criterio de selección de los documentos que se incluirán. Pero, a diferencia de los índices sparse a los que pretenden suplantar, el filtro no es necesariamente un campo objetivo del índice, como veremos. La ventaja es evidente: estos índices más pequeños ahorran memoria a la vez que tienen mejor rendimiento que un índice tradicional a la hora de escribir.
Vamos a crear una colección llamada discos en la que inyectaremos tres documentos:
db.discos.insertMany([{
"grupo": "The Who
"título": "Tommy",
"precio": 5
"disponible": true
}, {
"grupo": "AC/DC",
"título": "Powerage",
"precio: 8
"estado": null,
"disponible": false,
}, {
"grupo": "Queen",
"título": "Innuendo",
"precio": 9
"estado": "nuevo"...
Índices TTL
Los índices TTL tienen una duración limitada (Time To Live). Solo pueden aplicarse a un único campo, siempre que sea de tipo fecha. Los índices TTL se utilizan para eliminar documentos cuya fecha ha expirado. Son útiles para gestionar logs, sesiones, cestas o cualquier otra cosa con un tiempo de vida limitado. Para crearlos, la sintaxis es idéntica a la de un índice estándar; basta con especificar el número de segundos tras los cuales se considerará que el documento ha caducado.
El borrado de documentos caducados lo realiza el monitor TTL, una tarea en segundo plano que se ejecuta cada minuto. El valor predefinido del parámetro ttlMonitorSleepSecs puede consultarse mediante el siguiente comando de administración:
db.adminCommand({"getParameter":1, "ttlMonitorSleepSecs": 1})
De forma predefinida, nuestro monitor se invoca cada 60 segundos:
{ "ttlMonitorSleepSecs" : 60, "ok" : 1 }
Para cambiar el valor de este parámetro y ejecutar la tarea cada 30 segundos, es necesario cambiar el valor utilizando setParameter:
db.adminCommand({"setParameter":1, "ttlMonitorSleepSecs": 30})
Si se desea que se muestren los registros del monitor TTL, se deben activar mediante el siguiente comando:
db.setLogLevel(1, "index");
Si no se desea utilizar el monitor TTL, simplemente...
Índices agrupados (clustered index)
Un índice agrupado (clustered index) ordena los documentos de una colección en función del valor de su clave, cuya unicidad está garantizada. Las colecciones agrupadas (clustered collections) se caracterizan por este tipo de índices.
A diferencia de las colecciones «tradicionales», que almacenan los identificadores de los documentos en una estructura de datos en árbol distinta de aquella en la que se almacenan los propios documentos, las clustered collections reúnen todo en la misma estructura de datos; el campo del identificador del documento sirve de índice contra el que se encontrará el documento buscado, eliminando la necesidad de recorrer dos árboles para encontrarlo. De este modo, ¡solo tenemos que realizar una única operación para leer, insertar, borrar o actualizar!
Estas son algunas características especiales de los clustered indexes:
-
Sólo puede haber un clustered index para una colección y debe establecerse cuando se crea la colección.
-
Un clustered index se basa únicamente en el campo _id de la colección, pero este campo puede ser de un tipo distinto a ObjectId, siempre que se garantice que es único e inmutable. Sin embargo, el índice debe ser lo más pequeño posible y, si es posible, debe basarse en valores que aumenten secuencialmente.
Así...
Índices de texto
MongoDB proporciona un tipo especial de índice para buscar dentro de campos tipo texto o matricial que contengan texto. Para crear un índice de texto, ya no es cuestión de orden ascendente o descendente, como vimos anteriormente. Todo lo que hay que hacer es especificar delante del nombre del campo que su índice será de tipo text:
db.collection.createIndex({"campo": "text"})
Se debe tener en cuenta que una colección solo puede albergar un único índice textual, que sin embargo puede referirse a varios campos. Un índice textual no puede utilizar la intercalación.
Empecemos por crear la colección de libros, que utilizaremos para manipular nuestros índices de texto:
db.libros.insertMany([
{
"autor": "Jack London",
"título": "Colmillo Blanco",
"resumen": "Croc-Blanc es un orgulloso y valiente perro lobo",
"puntosDeVenta" : [
{ "ciudad": "Aix-en-Provence", "librería": "Goulard"}
]
},
{
"autor": "Stendhal",
"título": "Le Rouge et le noir",
"resumen": "El viaje de Julien Sorel"
},
{
"autor": ["Goscinny", "Uderzo"],
"título": "Astérix",
"resumen": "Las aventuras del orgulloso guerrero galo".
"puntosDeVenta" : [
{ "ciudad": "Marsella", "librería":...
Intersecar índices
La intersección de índices consiste en utilizar varios índices para satisfacer una consulta. Tomemos como punto de partida la colección meteo, que contiene dos documentos muy sencillos:
db.meteo.insertMany([
{
"ciudad": "Marsella",
"temperaturas": {"día": 38,5, "noche": 25},
"fecha": new Date("2024-08-15"),
"fiable": true
}, {
"ciudad": "París",
"temperaturas": {"día": 28,3, "noche": 19,6},
"fecha": new Date("2024-08-15"),
"fiable": true
}
])
Vamos a ponerle dos índices: el primero se dirige al campo día del documento temperaturas:
db.meteo.createIndex({"temperaturas.dia": -1})
Un segundo, compuesto, que se dirige primero al campo ciudad (ordenado alfabéticamente) y luego, en orden descendente, al campo...
El método explain
Este método muestra en pantalla un documento que contiene información sobre la planificación de la consulta y también puede mostrar estadísticas de ejecución. Tiene dos modos de funcionamiento: puede aplicarse a una colección o a un cursor.
Si se aplica a una colección, su sintaxis es la siguiente:
db.collection.explain(información).<método>
Si se detalla un cursor, adopta esta forma:
db.collection.find().explain(información)
En ambos casos, su único parámetro es una cadena de caracteres que especifica la naturaleza de la información que debe mostrarse. Este parámetro puede tomar los siguientes valores:
-
queryPlanner
-
executionStats
-
allPlansExecution
Si no especifica un parámetro a explain, la opción queryPlanner será el valor predefinido. Aquí hay algunos ejemplos de cómo hemos utilizado nuestra colección libros, comenzando con un cursor con la opción predefinida:
db.libros.find().explain()
Lo mismo, pero aplicado a la colección (el resultado será idéntico):
db.libros.explain().find()
Detalle de las estadísticas de ejecución, a nivel de colección:
db.libros.explain("executionStats").find()
Detalle de los planes de ejecución, siempre a nivel de colección:
db.libros.explain("allPlansExecution").find()
Independientemente de la opción pasada a explain(), podemos ver que la información relativa al planificador de consultas siempre está presente en el documento mostrado en pantalla. Si observamos más detenidamente el documento generado por el último explain(), podemos ver que toda la información está ahí: información relativa al planificador, estadísticas de ejecución y planes de ejecución, que se han añadido en forma de campo tipo tabla en el propio documento de estadísticas de ejecución. Estos distintos componentes se muestran a continuación en negrita:
"queryPlanner" :
"plannerVersion" : 1,
"namespace" : "test.libros",
"indexFilterSet" : false,
"parsedQuery"...
Forzar el empleo de un índice usando $hint
El operador $hint se utiliza para forzar una consulta a utilizar un índice determinado. Tiene un método abreviado en el shell, que se aplica a un cursor y tiene la siguiente forma:
db.collection.find( < criterios > ).hint( < índice > )
Si se enumera toda la colección de meteo y se aplica explain al cursor resultante, se verá que se realiza un COLLSCAN, lo cual parece lógico:
db.meteo.find().explain()
winningPlan: { stage: 'COLLSCAN', direction: 'forward' }
Ahora forcemos el uso del índice colocado en el campo ciudad y apliquemos explain a todo el conjunto para confirmar el cambio de estrategia del optimizador de consultas, que hemos forzado un poco:
db.meteo.find({}).hint({"ciudad": 1}).explain()
A partir de ahora, ¡el plan ganador es el tipo IXSCAN!
winningPlan: {
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { ciudad: 1 },
indexName: 'ciudad_1',
isMultiKey: false,
multiKeyPaths:...
Ocultar un índice usando hideIndex
Ocultar un índice consiste simplemente en desactivarlo, es decir, en asegurarse de que ya no puede ser utilizado por el planificador de consultas de MongoDB. Esta acción puede deshacerse, por lo que es una muy buena forma de comprobar las consecuencias de borrar un índice, sin tener que destruirlo y reconstruirlo, dos operaciones que pueden ser especialmente costosas para colecciones con muchos datos.
Supongamos que queremos desactivar temporalmente el índice del campo nombre de nuestra colección bazar, llamado idx_nombre:
db.bazar.hideIndex("idx_nombre");
Para validar esta operación, vamos a ejecutar el comando getIndexes en nuestra colección:
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { nombre: 1 }, name: 'idx_nombre', hidden: true }
]
Podemos ver que el booleano hidden está presente en el documento resultante de este comando, lo que significa que nuestro índice está ahora oculto. Una explicación sobre una búsqueda por nombre confirmará que MongoDB ahora debe realizar un COLLSCAN, ¡lo cual es cualquier cosa menos deseable!
Una vez que hemos validado que la eliminación del índice es extremadamente perjudicial para nuestras búsquedas más básicas, podemos reactivarlo...