Fecha publicación: 12 jul 2021

Conceptos básicos índices MongoDb

Empezar a jugar con MongoDb es muy fácil, y mientras trabajes con poco tráfico y un set de datos reducidos, tu motor de base de datos irá sobrado y tendrás un tiempo de respuesta razonable.

¿Qué pasa si empiezas a tener mucho tráfico, muchos datos o consultas complejas? Que tu servidor se empieza a cargar, tus consultas tardan mucho en resolverse y consumen muchos recursos... resumiendo que puedes empezar a tener problemas.

MongoDb ofrece una serie de mecanismos para mejorar el rendimiento de nuestras consultas, uno de los primeros de los que debemos aplicar es el de los índices.

Manos a la obra

Para este ejemplo vamos a usar el juego de datos de películas que los chicos de MongoDb ofrecen.

En nuestro caso nos hemos traido a nuestro servidor local esta información y la hemos restaurado en una base de datos, por si queréis hacer lo mismo, aquí la tenéis: (enlace al restore del dataset de ejemplo mflix de MongoDb):

Empezamos por seleccionar nuestra base de datos:

use mymovies

Vamos a contar cuantas películas hay en total

db.movies.count()

Y vemos que tenemos un total de 23530 películas, no esta mal.

Uno de los campos que tiene esta colección es el campo year, en el que podemos encontrar el año en que se estreno una película, vamos a contar cuantas películas se estrenaron en 2010

db.movies.count({ year: 2010 });

970 películas, hasta aquí todo parece bien, vamos a mostrar esos resultados:

db.movies.find({ year: 2010 });

Hemos hecho una consulta y hemos tenido una respuesta super rápida, también es cierto que la consulta es sencilla, la colección de películas tiene relatívamente pocos datos, y tenemos un portátil con recursos dedicados y sin apenas carga.

Vamos a ver si esta consulta se ha ejecutado de manera óptima, aquí le podemos pedir a mongo que nos explique como la ejecuta y cuanto ha tenido que trabajar para obtener los resultados, para ello añadimos la llamada explain a la consulta, y le añadimos como parámetro executionStats, de esta manera le estamos indicando que ejecute la consulta y nos devuelva estadísticas sobre dicha ejecución.

db.movies.find({ year: 2010 }).explain("executionStats");

Esto nos arroja un montón de información, vamos a quedarnos con las partes más interesantes, en concreto la sección executionStats que recoje las estadísticas de ejecución:

  • executionStats.executionStages.stage: fíjate que nos da el valor COLLSCAN, esto quiere decir que ha tenido que ir iterando documento por documento en la colección para encontrar las películas que se hubieran estrenado en 2010 ¿Es esto malo? Espérate a los siguientes puntos y verás.

  • executionStats.executionStages.nReturned: aquí nos dice cuantos documentos devuelve la consulta, en este caso hay 970 películas estrenadas en ese año.

  • executionStats.executionStages.totalDocsExamined: esto nos dice cuantos documentos ha tenido que recorrer mongo para poder encontrar los 970 documentos que cumplen con el filtro que hemos añadido en la consulta ¡ha tenido que leer la colección completa de películas (23530 documentos) para devolver sólo 970 documentos que cumplen con el criterio! ¡Esto huele mal!.

  • executionStats.executionStages.executionTimeMillis: este campo nos dice cuantos milisegundos ha tardado la consulta en ejecutarse, en este caso 18 milisegundos, puede parece muy poco, pero imagínate que pasaría con una colección que tuviera cientos de miles de entradas, un tráfico de usuarios elevado o unas consultas más complejas, nos podría reventar en la cara, ¿Hasta donde podemos bajar el tiempo de ejecución de una consulta? vamos a ver que podemos hacer.

{ queryPlanner:
   { plannerVersion: 1,
     namespace: 'mymovies.movies',
     indexFilterSet: false,
     parsedQuery: { year: { '$eq': 2010 } },
     winningPlan:
      { stage: 'COLLSCAN',
        filter: { year: { '$eq': 2010 } },
        direction: 'forward' },
     rejectedPlans: [] },
  executionStats:
   { executionSuccess: true,
     nReturned: 970,
     executionTimeMillis: 18,
     totalKeysExamined: 0,
     totalDocsExamined: 23530,
     executionStages:
      { stage: 'COLLSCAN',
        filter: { year: { '$eq': 2010 } },
        nReturned: 970,
        executionTimeMillisEstimate: 2,
        works: 23532,
        advanced: 970,
        needTime: 22561,
        needYield: 0,
        saveState: 23,
        restoreState: 23,
        isEOF: 1,
        direction: 'forward',
        docsExamined: 23530 } },
  serverInfo:
   { host: '1dc2f449f0ac',
     port: 27017,
     version: '4.4.3',
     gitVersion: '913d6b62acfbb344dde1b116f4161360acd8fd13' },
  ok: 1 }

¿No hay forma de que MongoDb sea un poco más listo y no tenga que recorrer todos los documentos para encontrar algo? Por ejemplo cuando leemos un libro técnico y queremos buscar ciertos términos, no nos ponemos página por página a buscar la palabra en cuestión, ...nos vamos al final del libro, y nos encontramos un índice por palabra que nos permite encontrar ese termino clasificado por alfabético, y saltar directamente a las páginas donde esta el contenido.

Un índice en MongoDb funciona de una manera parecida: se monta una estructura aparte en la que con arboles beta, se organiza por uno o más campos la información que hay en la colección, así, si por ejemplo, creamos un índice por el campo año y hacemos una búsqueda sobre el año 2010, el query planner de MongoDb detectará que existe un índice para ese campo y podrá de forma directa ir a los documentos en concreto sin tener que ir entrada por entrada viendo si la película es de ese año o no.

Vamos a ver esto en acción.

Creamos un índice sobre el campo year (año de estreno de la película):

db.movies.createIndex({ year: 1 });

Aquí le estamos indicando que cree un índice sobre el campo year, y con el valor 1 le decimos que lo ordene de forma ascendente.

¿Habremos ganado algo al crear esto? Toca comprobarlo, lanzamos la consulta y le pedimos a mongo que nos explique que tal ha ido.

db.movies.find({ year: 2010 }).explain("executionStats");

Veamos el resultado

  • executionStats.executionStages.stage: aquí nos da un FETCH para decir que está obteniendo los documentos y más abajo en inputStage podemos ver otro valor IXScan que nos índica que está tirando de un índice (el que acabamos de crear)
  • executionStats.executionStages.nReturned: Aquí no cambia nada, la consulta sigue devolviendo las 970 películas que se estrenaron en 2010, esto es correcto.
  • executionStats.executionStages.totalDocsExamined: si comparamos con el resultado anterior, en este damos el campanazo, antes teníamos que leer más de 20.000 documentos para encontrar las películas, ahora fíjate, sólo ha tenido que recorrer 970 películas que son justo las que tenía que devolver en la consulta.
  • executionStats.executionStages.executionTimeMillis: si comparamos tiempos, la mejora es brutal, de 18 milisegundos hemos bajado a 2 milisegundos, ¿no está mal verdad?
{ queryPlanner:
   { plannerVersion: 1,
     namespace: 'mymovies.movies',
     indexFilterSet: false,
     parsedQuery: { year: { '$eq': 2010 } },
     winningPlan:
      { stage: 'FETCH',
        inputStage:
         { stage: 'IXSCAN',
           keyPattern: { year: 1 },
           indexName: 'year_1',
           isMultiKey: false,
           multiKeyPaths: { year: [] },
           isUnique: false,
           isSparse: false,
           isPartial: false,
           indexVersion: 2,
           direction: 'forward',
           indexBounds: { year: [ '[2010, 2010]' ] } } },
     rejectedPlans: [] },
  executionStats:
   { executionSuccess: true,
     nReturned: 970,
     executionTimeMillis: 2,
     totalKeysExamined: 970,
     totalDocsExamined: 970,
     executionStages:
      { stage: 'FETCH',
        nReturned: 970,
        executionTimeMillisEstimate: 0,
        works: 971,
        advanced: 970,
        needTime: 0,
        needYield: 0,
        saveState: 0,
        restoreState: 0,
        isEOF: 1,
        docsExamined: 970,
        alreadyHasObj: 0,
        inputStage:
         { stage: 'IXSCAN',
           nReturned: 970,
           executionTimeMillisEstimate: 0,
           works: 971,
           advanced: 970,
           needTime: 0,
           needYield: 0,
           saveState: 0,
           restoreState: 0,
           isEOF: 1,
           keyPattern: { year: 1 },
           indexName: 'year_1',
           isMultiKey: false,
           multiKeyPaths: { year: [] },
           isUnique: false,
           isSparse: false,
           isPartial: false,
           indexVersion: 2,
           direction: 'forward',
           indexBounds: { year: [ '[2010, 2010]' ] },
           keysExamined: 970,
           seeks: 1,
           dupsTested: 0,
           dupsDropped: 0 } } },
  serverInfo:
   { host: '1dc2f449f0ac',
     port: 27017,
     version: '4.4.3',
     gitVersion: '913d6b62acfbb344dde1b116f4161360acd8fd13'
    },
  ok: 1
}

Ahora es cuando podemos estar pensando "¡ala!, pues voy a crear índices para todos los campos"... cuidado, crear índices no es gratis:

  • Hace más lentas las escrituras (cada vez que insertamos o actualizamos un documento puede que tengamos que actualizar el índice).
  • Los índices consumen recursos (disco duro y memoria).
  • No todos los índices son igual de efectivos, depende mucho de las consultas que estemos lanzando.

Esto hace que tengamos que evaluar bien qué índices crear, estudiando como se está explotando la información en nuestra base de datos. En una aplicación real, que tenga cierto volumen y tráfico, es fundamental chequear estadísticas de uso y asegurarnos que elegimos los índices correctos para tener un buen rendimiento de nuestro sistema.

¿Con ganas de aprender Backend?

En Lemoncode impartimos un Bootcamp Backend Online, centrado en stack node y stack .net, en él encontrarás todos los recursos necesarios: clases de los mejores profesionales del sector, tutorías en cuanto las necesites y ejercicios para desarrollar lo aprendido en los distintos módulos. Si quieres saber más puedes pinchar aquí para más información sobre este Bootcamp Backend.