Skip to content

4 Agregación Pipeline

Introducción

La agregación pipeline o tuberías de agregación se basa en someter una colección a un conjunto de operaciones o etapas, estas etapas irán convirtiendo y transformando el conjunto de documentos pertenecientes a la colección, hasta obtener un conjunto de documentos con el resultado deseado.

Se le llama tubería ya que cada etapa irá modificando, moldeando y calculando la estructura de los documentos para pasarlos a la etapa que le sigue.

Funciones de agregado

Al igual que en otra base de datos, MongoDB dispone de funciones matemáticas y de cadenas para utilizaras en las consultas.

Funciones Aritméticas

Función Descripción
$abs Valor absoluto de un número
$add Añade números a una cantidad o a una fecha, en este caso suma milisegundos.
$ceil Devuelve el entero menor, mayor o igual que el número especificado.
$divide Devuelve el resultado de dividir el primer número por el segundo
floor Devuelve el entero mayor, menor o igual que el número especificado.
$mod Devuelve el resto de dividir el primero número por el segundo
$multiply Multiplica varios números, acepta varios argumentos.
$pow Eleva un número a la potencia especificada
$sqrt Calcula la raíz cuadrada
$subtract Devuelve el resultado de restar el primer número menos el segundo. Si los dos valores son fechas devuelve la diferencia en milisegundos.
$trunc Trunca un número

Funciones de Cadenas

Función Descripción
$concat Concatena varias cadenas
$substr Devuelve una subcadena, a partir de una posición indicada hasta una longitud especificada.
$toLower Convierte una cadena a minúsculas
$toUpper Convierte una cadena mayúsculas
$strcasecmp Compara cadenas y devuelve 0 si ambos son iguales, 1 si la primera es mayor que la segunda, o -1 si la primera es menor que la segunda

Funciones de Grupo

Función Descripción
sum Devuelve la sma de los valores numéricos, ignorando los no numéricos.
$avg Devuelve la media de los valores numéricos e ignora los no numéricos
$first Devuelve el primer valor del grupo
$last Devuelve el último valor
$max Devuelve el valor máximo
$min Devuelve el valor mínimo

Funciones de Fecha

Función Descripción
$dayOfYear Devuelve el día del año
$dayOfMonth Devuelve el día del mes.
$dayOfWeek Devuelve el día de la semana
$year Devuelve el año
$month Devuelve el número del mes
$hour Devuelve la hora
$minute Devuelve los minutos
$second Devuelve los segundos
$dateToString Devuelve la fecha en String

Uso de estas funciones

Estas funciones se utilizan en las operaciones de agregación, o consultas de agregación, que lo que hacen es procesar los registros y obtener nuevos resultados, calculados o transformados.

La agregación opera con grupos de valores de múltiples documentos y se puede realizar una variedad de operaciones sobre los datos agrupados para devolver un solo resultado. El objetivo es presentar datos calculados, formateados y/o filtrados de manera diferente a como se encuentra en los documentos.

MongoDB ofrece tres formas de realizar la agregación: la agregación pipeline, la función de map-reduce y la agregación de propósito único.

Etapas Pipeline

Las etapas son las siguientes:

Etapa Descripción Multiplicidad
$match Filtra los resultado. Permite filtrar los resultados de los documentos que cumplen cierto criterios. Se puede filtrar antes o después de agregar resultados. n:1
$group Agrupación. Permite agrupar distintos documentos según compartan el valor de uno o varios de sus atributos, y realizar operaciones sobre los elementos de cada uno de los grupos. Se utilizan las funciones de grupo n:1
$sort Ordenación de los documentos 1:1
$skip Salta N documentos n:1
$limit Elige N elementos para el resultado n:1
$unwind Normaliza arrays 1:n
$out Envía el resultado a una salida, se almacena en la BD como una nueva colección 1:1

La multiplicidad se refiere a cuántos documentos obtenemos como resultado después de aplicar la etapa. De esta forma 1:1 se aplica a un documento y se obtiene otro, n:1 se aplica a n documentos y se obtiene 1, y 1:n se aplica a un documentos y se obtienen n.

Formato pipeline

El formato de pipe line es la siguiente:

db.collection.aggregate([
    {
     $etapa1: {
      //...
     }
    }, {
     $etapa2: {
      //...
     }
    },
])

Por ejemplo:

Tenemos la colección articulos, cada registro está formado por los campos: código, denominación, pvp, categoría, uv, y stock.

Vamos a obtener los articulos de los artículos y la categoría convertida a mayúsculas. Se utiliza la etapa $project pues cambiamos el aspecto del documento.

¡Importante!

Para referirnos a los campos del documento los ponemos entre comillas y con el prefijo $.

db.articulos.aggregate([
    {
     $project:
     {
      denominacion: {$toUpper: "$denominacion"}
      cetegoria: {$toUpper: "$categoria"}
     }
    }
])

Si dicha salida se desea añadir a la base de datos, se le añade la etapa out:

db.articulos.aggregate([
    {
     $project:
     {
      denominacion: {$toUpper: "$denominacion"}
      cetegoria: {$toUpper: "$categoria"}
     }
    },
    { $out: "salidanueva"}
])

Vamos a obtener la denominación en mayúsculas, el importe de las ventas que serán las uv * pvp, y el stock actual que será stock - uv.

Las etapas a usar en este caso son: project, multiply, substract.

db.articulos.aggregate([
    {
    $project: {
     articulo: {$toUpper: "$denominacion"},
     importe: { $multiply: ["$pvp", "$uv"]},
     stockactual: {$subtract: ["$stock", "$uv"]}
    }
    }
])

Condiciones de agregación

Podemos añadir las siguientes condiciones a las consultas de agregación

Name Descripción
$cond Este operador evalúa una expresión y dependiendo del resultado, devuelve el valor de una de las otras dos expresiones. Recibe tres expresiones en una lista ordenada:
{$cond: [ \, \, \]}
$ifNull Devuelve o bien el resultado no nulo de la primera expresión o el resultado de la segunda expresión si la primera expresión da como resultado un resultado nulo. El resultado de la segunda expresión puede ser nulo.
{ $ifNull: [ \, <expresionsiesnull>]}

Ejemplo: A la consulta anterior, vamos a preguntar si el stock actual es negativo, asignaremos a un campo nuevo llamado reponer true si es menor que 0 y false si no lo es:

db.articulos.aggregate([
    {
    $project: {
     articulo: {$toUpper: "$denominacion"},
     importe: { $multiply: ["$pvp", "$uv"]},
     stockactual: {$subtract: ["$stock", "$uv"]},
     reponer: {
      $cond: [
             {$lte: [{$substract: ["stock", "uv"]}, 0]}, true, false
         ] 
     }
    }}
])

En la siguiente consulta, obtenemos por cada categoría el número de artículos, el total unidades vendidas de artículos, y el total importe, la suma de los pvp*unidades. Se utiliza la etapa $group, cuando se usa esta etapa se debe añadir el identificador de objeto _id, en este caso como agrupamos por categoría lo indicamos en el _id. Para contar artículos se utiliza la función $sum

db.articulos.aggregate([
    {
    $group:{
     _id: "$categoria",
     contador: {$sum: 1},
     sumaunidades: {$sum: "$uv"},
     totalimporte: {$sum: {$multiply: ["$pvp", "$uv"]}}
    }
    }
])

En la siguiente consulta obtenemos el número de documentos de la categoría Deportes, el total de unidades vendidas de sus artículos, el total del importe y la media de unidades vendidas. Se utilizan las etapas $match para seleccionar la categoría, y luego $group para obtener resultados agrupados

db.articulos.aggregate([
    {$match: {categoria: "Deportes"}},
    {$group: {
     _id: "Deportes",
     contador: {$sum: 1},
     sumaunidades: { $sum: "$uv"},
     media: { $avg: "$uv"},
     totalimporte: { $sum: { $multiply: ["$pvp", "$uv"]}}
    }}
])

Obtenemos, ahora el precio más caro:

db.articulos.aggreate([
    {$group: {
     _id: null,
     maximo: { $max: "$pvp"}
    }}
])

¿Y sí, queremos obtener el artículo con el precio más caro?

En primer lugar, debemos obtener los datos pvp y denominación, de todos los articulos ordedanos descendentemente por el precio y denominación. ($sort)

Después, el resultado obtenido se agrupa para luego coger el primero con la función $first

db.articulos.aggregate([
    { $sort: { pvp: -1, denominacion: -1}}
    {
     $group:{
      _id:null,
      mascaro: {$first: "$denominacion"}
      precio: {$first: "$pvp"}
     }
    }
])

Ahora, queremos obtener la suma del importe de los artículos cuya denominación empieza por M o P.

Primero, debemos obtener el primer carácter de todos los artículos utilizando la función $substr:

{
 $project: {
  primercarac: { $substr: ["$denominacion", 0, 1]},
  impor: { $multiply: ["$pvp", "$uv"]}
 }
}

Después seleccionamos, de los datos obtenidos, lo que tienen en primercarac P o M:

{$match: {"primercarac": {$in: ["M", "P"]}}}

Finalmente, se agrupa el resultado y se suma los importes:

{
 $group:{
  _id: 1,
  totalimporte: { $sum: "$impor"}
 }
}

La consulta completa sería:

db.articulos.aggregate([
    {
  $project: {
   primercarac: { $substr: ["$denominacion", 0, 1]},
   impor: { $multiply: ["$pvp", "$uv"]}
  }
 },
    {$match: {"primercarac": {$in: ["M", "P"]}}},
    {
        $group:{
            _id: 1,
   totalimporte: { $sum: "$impor"}
  }
 }
])

En la siguiente consulta, obtenemos por cada categoría el artículo con el precio más caro. Para ello primero ordenamos descendentemente por pcategoría, pvp y denominación, utilizando la etapa $sort. Y el resultado obtenido se agrupa con $group para luego obtener el primero de cada categoría con la función $first

db.articulos.aggregate([
    {$sort: {categoria: -1, pvp: -1, denominacion: -1}},
    {
     $group:{
      _id: "$categoria",
      mascaro: { $first: "$denominacion"},
      precio: {$first: "$pvp"}
     }
    }
])

Utilización de Arrays, Campos compuestos y agregados

Las siguientes funciones sirve trabajar con arrays:

Nombre Descripción
$arrayElemAt Devuelve el elemento especificado en el índice
$concatArrays Devuelve un array concatenado en una cadena
$filter Selecciona elementos de un array y devuelve otro array con esos elementos.
$isArray Determina si el operando es un array o no. Devuelve true o false
$size Devuelve el número de elementos del array
$slice Devuelve un sub-set de elementos del array, especificando el número

Vamos a usar ahora la colección Trabajadores. Vamos a insertar un nuevo trabajador

db.trabajadores.insert(
    {
  nombre: {nom: "Alicia", ape1: "Ramos", ape2: "Martín"},
     direccion: {poblacion: "Madrid", calle: {"Avda Toledo 10", }},
     salario: 1200,
     oficios: ["Profesora", "Analista"],
     primas: [20,30,40],
     edad: 50
    }
)

Como podemos observar un empleado, tiene dos campos compuestos, nombre y dirección, y además dos listas: oficios y primas.

Vamos a consultar la población, el nombre descompuesto en nombre, ape1 y ape2, el primer oficio, el segundo y el último. En caso de no tenerlas no devuelve nada. Además lo vamos a ordenar de forma ascendente

Importante

Para acceder a los campos compuestos navegamos como si fuese un objeto. Por ejemplo, si quiero acceder al campo nom del nombre o al campo poblacion de la dirección, sería: nombre.nom o direccion.poblacion

db.trabajadores.find({direccion.poblacion: "Toledo"})
db.trabajadores.aggregate([
    {$sort: {direccion.poblacion: 1}},
    {$project: 
     {
      poblacion: "$direccion.poblacion",
      nombre: "$nombre.nom",
      ape1: "$nombre.ape1",
      ape2: "$nombre.ape2",
      oficio1: {$arrayElementAt: ["$oficios", 0]},
      oficio2: {$arrayElementAt: ["$oficios", 1]},
      oficio3: {$arrayElementAt: ["$oficios", -1]}
     }
    }
])

Ahora, obtenemos los elementos que tienen los arrays de los trabajadores, y los arrays concatenados. Se utiliza la función $ifNull para comprobar que los arrays existan en los trabajadores, y evitar errores de salida. Se pregunta si es null, si lo es devuelve el array vacio.

db.trabajadores.aggregate([
    {
    $project:
     {
      nombre: "$nombre.nom",
      numerooficios: {$size: {"$ifNull": ["$oficios", []]}},
      numeroprimas: { $size: { "$ifNull": ["$primas", []]}},
      oficiosconcatenados: {$concatArrays: ["$oficios", "$primas"]}
     }
    }
])

Por último, devolvemos el número de trabajadores y la media de edad de los trabajadores que han tenido el oficio de Analista

db.trabajadores.aggregate([
    {$match: {oficios: "Analista"}},
    {$group: {
     _id: "analista",
     contador: { $sum:1 },
     media: {$avg: "$edad"}
    }}
])
Ejercicio

Utilizando la colección trabajadores realiza las siguientes consultas:

  • Visualiza la edad media, la media del salario y el número de trabajadores que hayan tenido una prima de 30 o de 80.
  • Visualiza por población el número de trabajadores, el salario medio y el máximo salario.
  • Visualiza el nombre, ape1, ape2, del empleado que tiene máximo salario
  • A partir de la consulta anterior, obtén ahora el nombre, ape1, ape2 y salario del empleado que tiene máximo salario por cada población.