MongoDB: Advanced Querys

Start wide, expand further, and never look back.

Arnold Schwarzenegger

En nuestro post anterior, aprendimos a realizar las operaciones básicas CRUD sobre una base de datos MongoDB, incluyendo consultas básicas de igualdad, desigualdad numérica (mayor y menor que) y conjunción y disyunción (or y and). Sin embargo, estos criterios de búsqueda rara vez serán suficiente para satisfacer nuestros requerimientos, por lo que en este post nos dedicaremos a explorar los otros operadores y criterios existentes en MongoDB para el comando find().

Antes que nada procedemos a conectarnos a una base de datos y a ingresar algunos datos de prueba. Para nuestro ejemplo, ingresamos los siguientes  comandos en nuestra terminal:

use estudiantes

 

db.estudiantes.insertOne({nombre : 'Juan', apellido : 'Linarez', edad : 21, notas : [18,19,16], programa : { nombre : 'Ingeniería en Informática', nivel : 'pregrado'}, grupos: [ { nombre : 'Club de  Ajedrez', descripción : 'Grupo de jugadores de Ajedrez'}, { nombre : 'Club de Informática', descripcion : 'Grupo para fanáticos de Tecnología'}] })

db.estudiantes.insertOne({nombre : 'Maria', apellido : 'Cadenas', edad : 18, notas : [12,14], programa : { nombre : 'Medicina', nivel : 'pregrado'}, grupos: [ { nombre : 'Club de  Volleyball', descripción : 'Equipo d eVolleyball de la universidad.'}]})

db.estudiantes.insertOne({nombre : 'Jorge', apellido : 'Rodriguez', edad : 31, notas : [14,19,13], programa : { nombre : 'Ingeniería Civil', nivel : 'postgrado'}})

db.estudiantes.insertOne({nombre : 'Carla', apellido : 'Sandoval', edad : 24, notas : [15,17,16,15], programa : { nombre : 'Ingeniería en Informática', nivel : 'pregrado'}, grupos: [ { nombre : 'Club de Informática', descripcion : 'Grupo para fanáticos de Tecnología'}] })

db.estudiantes.insertOne({nombre : 'William', apellido : 'Smith', edad : 14, notas : ['A', 'B+', 'A-'], programa : { nombre : 'Bachiller'}, grupos: [ { nombre : 'Club de PS4', descripción : 'Grupo de jugadores de Playstation 4'}] })

Copy to Clipboard

NOTA: Como ya sabemos, podemos agregarlos todos utilizando insertMany(), pero para mantener la legibilidad lo dejaremos de esta forma.

 

Proyecciones:

 Si queremos solamente obtener algunos datos de la consulta y no los objetos completos, pasamos como segundo parámetro un documento con las proyecciones que deseemos mostrar, esto es, un documento que tendrá duplas campo-valor el campo que deseemos mostrar y en valor tendrá 0 si no deseamos mostrarlo o 1 si deseamos mostrarlo. Si solo especificamos campos con 1, solo mostrara esos campos, y si solo especificamos campos con 0, mostrará todos los campos a excepción de estos.

NOTA: Solo podemos o mostrar n cantidad de elementos, o no mostrar n cantidad de elementos, no podemos hacer una mezcla de elementos a mostrar y ocultar, es decir, no podemos mezclar algunos campos con 1 y otros con 0. La única excepción a esta regla es el campo _id, el cual como siempre se muestra por defecto, podemos agregarlo a una proyección con campos a mostrar para que no lo muestre.

Por ejemplo (y aquí aprovecharemos para repasar los comandos básicos del post anterior):

Si queremos obtener solamente el nombre de aquellos estudiantes que sean mayores a 20 y menores a 30 años ejecutaríamos el siguiente comando:

 

db.estudiantes.find({edad: { $gt : 20, $lt : 30}},{nombre:1})

Copy to Clipboard

Ahora, si queremos obtener toda la información a excepción del programa y los grupos de todos los estudiantes que sean menores a 20 o mayores a 30 años, usaríamos el siguiente:

 

db.estudiantes.find({ $or : [{edad : {$gt : 30}},{edad : {$lt : 20}}]},{programa:0, grupos:0})

 

Operadores:

 Además de los operadores que ya conocemos (igualdad, $gt, $lt) existen otros que veremos a continuación:

$gte y $lte son similares a $gt y  $lt pero el criterio de igualdad también los satisface a diferencia de los otros. Sería decir que son “mayor o igual que…” y “menor o igual que…”

$in:  nos permite verificar si el valor del campo buscar se encuentra dentro de un arreglo específico.

Ej: Si queremos obtener los documentos donde el nombre sea o “María”, o “José”, “Juan” o 10. Lo haríamos de la siguiente forma:

 

db.estudiantes.find({ nombre : { $in : ['Maria','José','Juan', 10] } },{nombre: 1, _id: 0})

NOTA: Recordar que como no existe un esquema en la base de datos, el tipo de dato de un campo específico no tiene que ser el mismo en todos los documentos, pueden haber documentos con notas numéricas y otras con notas alfabéticas como se tiene en el ejemplo.

$all: nos permite obtener todos los documentos que contengan un arreglo específico en el campo especificado.

Ej: Si queremos obtener todos los estudiantes con las notas 18, 19 y 16. Podemos utilizar el siguiente comando:

 

db.estudiantes.find({notas:[18,19,16]},{nombre:1,notas:1,_id:0})

Copy to Clipboard

$regex o /regex/: nos permite utilizar una expresión regular para ejecutar una búsqueda. Es con este comando que podemos hacer un equivalente al “LIKE” de SQL.

Ej: Si queremos obtener todos los estudiantes cuyo nombre empiece por la letra “J”, usemos el siguiente comando:

 

db.estudiantes.find({nombre:{$regex:/^J/}},{nombre:1,_id:0})

O si queremos obtener todos los estudiantes cuyo apellido contenga una “n”, usemos este otro:

 

db.estudiantes.find({apellido:/n/},{apellido:1,_id:0})

 

Búsqueda en documentos internos:

Si queremos realizar búsquedas en documentos internos, lo que sería el equivalente a un join en una base de datos relacional, basta con acceder al campo del documento de la misma forma que accederíamos en un objeto JSON, utilizando notación con punto (.).

NOTA: En este caso debemos obligatoriamente pasar el campo como un string poniéndolo dentro de comillas.

Ej: Si quisiéramos obtener los estudiantes con programa de nivel “pregrado”, lo haríamos de la siguiente forma:

 

db.estudiantes.find({'programa.nivel' : 'pregrado'},{nombre:1,apellido:1,programa:1,_id:0})

 

Dentro de esta sentencia también podemos utilizar todos los demás operadores que ya vimos. Por ejemplo, si quisiéramos obtener los estudiantes con un programa cuyo nombre empiece por la letra  “I”, y que además tenga nivel “pregrado”, podríamos utilizar el siguiente comando:

 

db.estudiantes.find({'programa.nivel' : 'pregrado', 'programa.nombre' : /^I/ },{nombre:1,apellido:1,programa:1,_id:0})

 

También podemos mezclar búsqueda en campos normales y documentos internos del mismo documento.

Ej: Si quisiéramos obtener los estudiantes mayores a 20 años con un programa de nivel pregrado lo haríamos de la siguiente forma:

 

db.estudiantes.find({'programa.nivel' : 'pregrado', edad : {$gt : 20} },{nombre:1,apellido:1,programa:1,_id:0, edad:1})

 

Búsqueda en arreglos internos

Como ya vimos previamente, podemos utilizar el operador de igualdad para buscar arreglos exactamente iguales y el operador $all para buscar arreglos que tengan todos los elementos de otro arreglo.

Por el contrario, si quisiéramos buscar los documentos en los que fuera suficiente que alguno de los elementos dentro de un arreglo interno cumpla con un criterio, podríamos hacerlo sencillamente utilizando el criterio de igualdad. Si el campo sobre el que estamos buscando es un arreglo, retornara los documentos en los que cualquiera de los elementos del arreglo cumpla el criterio.

Ej: Si quisiéramos encontrar los estudiantes que tienen en una de sus notas un 19, haríamos lo siguiente:

 

db.estudiantes.find({notas: 19},{nombre:1,apellido:1,notas:1,_id:0})

 

De la misma forma podemos utilizar los demás operadores que ya conocemos. Si quisiéramos obtener los estudiantes con al menos una nota mayor o igual a 17, usaríamos este comando:

 

db.estudiantes.find({notas: {$gte : 17}},{nombre:1,apellido:1,notas:1,_id:0})

 

O si quisiéramos utilizar una disyunción y obtener los estudiantes con alguna de sus notas mayor o igual a 12 y menor a 16 utilizaríamos este otro:

 

db.estudiantes.find({notas: {$gte : 12, $lt : 16}},{nombre:1,apellido:1,notas:1,_id:0})

 

$elemMatch: este operador lo utilizamos cuando queremos verificar que un criterio de búsqueda se aplique sobre todos los elementos de un arreglo, por lo que lo podemos utilizar para obtener el mismo resultado que el comando anterior de la siguiente forma:

 

db.estudiantes.find({notas: {$elemMatch : {$gte:12,$lt:16}}},{nombre:1,apellido:1,notas:1,_id:0})

 

Por otra parte, también podemos acceder y ejecutar criterios de búsqueda directamente sobre un elemento específico de un arreglo accediendo a él a través de su índice de la misma forma que lo haríamos con objetos incrustados.

Ej: si quisiéramos saber los estudiantes cuya primera nota es 12, haríamos lo siguiente:

 

db.estudiantes.find({'notas.0' : 12},{nombre:1,apellido:1,notas:1,_id:0})

 

NOTA: Como es un arreglo, el índice del primer elemento es el 0 y por lo tanto, el índice del n-ésimo elemento siempre sera n -1. Además, nótese como se pone el campo entre comillas para que sea tratado como un string al igual que si fuera un objeto incrustado.

Como último operador de arreglos, tenemos el $size, el cual  nos permite obtener el tamaño de un arreglo para poder hacer búsquedas en base a este.

Ej: Si quisiéramos saber los estudiantes con 3 notas exclusivamente, lo haríamos de la siguiente forma:

 

db.estudiantes.find({ notas : {$size : 3}},{nombre:1,apellido:1,notas:1,_id:0})

 

Búsqueda en documentos incrustados en arreglos internos:

Finalmente también podemos hacer búsquedas dentro documentos incrustados en arreglos internos utilizando el operador de igualdad o el operador $elemMatch.

Si utilizamos el operador de igualdad, estaremos buscando un objeto incrustado en un arreglo que sea exactamente igual al que pasamos como parámetro.

Ej: El siguiente comando no me retorna los estudiantes que pertenecen al grupo “Club de Ajedrez”:

 

db.estudiantes.find({ grupos : {nombre:"Club de  Ajedrez"}},{nombre:1,apellido:1,_id:0, grupos:1})

 

Para lograr ese objetivo debo pasar el objeto completo, es decir, también debo especificar la descripción de la siguiente forma:

 

db.estudiantes.find({ grupos : {nombre:"Club de  Ajedrez", descripción: "Grupo de jugadores de Ajedrez"}},{nombre:1,apellido:1,_id:0, grupos:1})

 

Al igual que en los arreglos normales, podemos hacer búsquedas sobre un elemento específico utilizando su índice de la siguiente forma:

Ej: si quisiera obtener los estudiantes cuyo primer grupo fuera el de nombre “Club de Informática”, lo haría de la siguiente forma:

 

db.estudiantes.find({ 'grupos.0.nombre' : "Club de Informática"},{nombre:1,apellido:1,_id:0, grupos:1})

 

NOTA: Nuevamente no olvidar las comillas.

Ahora, si no queremos especificar el índice y queremos hacer una búsqueda general, lo hacemos sencillamente accediendo al campo directamente desde el nombre del arreglo sin pasar el índice.

Ej: si queremos obtener los estudiantes que pertenecen al grupo de nombre “Club de Informática”, hacemos lo siguiente:

 

db.estudiantes.find({ 'grupos.nombre' : "Club de Informática"},{nombre:1,apellido:1,_id:0, grupos:1})

Copy to Clipboard

De la misma forma, también podemos utilizar el operador $elemMatch para hacer búsquedas con múltiples criterios sobre alguno de los objetos incrustados.

Ej: Si queremos ver los estudiantes que pertenecen o al club de informática o al club de volleyball, hacemos lo siguiente:

 

db.estudiantes.find({ grupos : {$elemMatch : { $or : [{nombre:"Club de Informática"},{nombre: "Club de  Volleyball"}] } } },{nombre:1,apellido:1,_id:0, grupos:1})

 

Finalmente, pongamos a prueba los conocimientos obtenidos haciendo una consulta más compleja.

Para obtener los estudiantes menores a 30 años, que tengan 3 notas registradas de las cuales al menos una sea mayor o igual a 18, estén en un programa de nivel “pregrado” y pertenezcan al curso de ajedrez o al de volleyball sin mostrar sus nombres pero si sus apellidos, haríamos lo siguiente:

 

db.estudiantes.find({ edad : { $lt : 30 }, notas : { $size : 3, $gte : 18 }, 'programa.nivel' : 'pregrado', grupos : { $elemMatch : { nombre : { $in : ['Club de  Ajedrez', 'Club de  Volleyball' ]}} } },{nombre:0,_id:0})

 

Exists:

Como último comando, el comando $exists lo podemos pasar como un valor booleano para saber si existe un campo específico en un documento.

Ej: Si quisiéramos saber los estudiantes que no pertenecen a ningún grupo, podríamos hacer lo siguiente:

 

db.estudiantes.find({grupos: {$exists : false}})

 

Existen otros operadores y comandos en MongoDB pero con los ya explicados creo que podemos satisfacer la tradicional regla de Paretto (80/20). Pero si quieres saber sobre algún otro comando no mencionado, o si tienes alguna duda sobre alguno de los que si hablamos, no dudes en comentar.

 

Publicado el 09 de mayo de 2019