Manipulación de datos en MongoDB mediante Aggregation Pipeline.

2
26116

0. Índice de contenidos.

1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Ordenador iMac 27″ (3.2 GHz Intel Core i5, 8 GB DDR3)
  • Sistema Operativo: Mac OS X Mavericks 10.9

2. Introducción.

Este es es tercer tutorial de una serie que estoy haciendo sobre MongoDB. Podéis ir a estos enlaces como punto de partida: el primero, o el segundo.

En este caso vamos a ver cómo procesar estructuras complejas de datos para conseguir subtotales por distintos criterios con estructuras complejas e incluso cómo almacenar estos datos en otras estructuras/colecciones.

Os recomiendo descargar la documentación de Aggregation del site de mongoDB en formato pdf: http://docs.mongodb.org/manual/aggregation/

Con Aggregation Pipeline, a través de la concatenación de comandos, vamos obteniendo conjuntos de documentos intermedios que podemos transformar.

El juego de datos de partida que manejaremos será el siguiente, el mismo que en el anterior tutorial:

/* 0 */
{
    "_id" : ObjectId("529f4fcc21c58ff03c428c6e"),
    "nombre" : "Roberto",
    "apellido" : "Canales",
    "peso" : 80,
    "sede" : "Madrid",
    "puesto" : "Desarrollo",
    "gastos" : [ 
        {
            "fecha" : "Enero",
            "Comercio" : "ElCorteDeManga",
            "Compra" : [ 
                {
                    "Concepto" : "Electronica",
                    "Importe" : 1000
                }, 
                {
                    "Concepto" : "Alimentación",
                    "Importe" : 2000
                }, 
                {
                    "Concepto" : "Deportes",
                    "Importe" : 2000
                }
            ]
        }, 
        {
            "fecha" : "Febrero",
            "Comercio" : "Garranfur",
            "Compra" : [ 
                {
                    "Concepto" : "Electronica",
                    "Importe" : 400
                }, 
                {
                    "Concepto" : "Alimentación",
                    "Importe" : 1000
                }, 
                {
                    "Concepto" : "Deportes",
                    "Importe" : 2000
                }
            ]
        }
    ]
}

/* 1 */
{
    "_id" : ObjectId("529f503121c58ff03c428c6f"),
    "nombre" : "Jose Maria",
    "apellido" : "Toribio",
    "peso" : 78,
    "sede" : "Madrid",
    "puesto" : "Operaciones",
    "gastos" : [ 
        {
            "fecha" : "Enero",
            "Comercio" : "EFVNAF",
            "Compra" : [ 
                {
                    "Concepto" : "Electronica",
                    "Importe" : 1000
                }, 
                {
                    "Concepto" : "Deportes",
                    "Importe" : 2000
                }
            ]
        }, 
        {
            "fecha" : "Febrero",
            "Comercio" : "Garranfur",
            "Compra" : [ 
                {
                    "Concepto" : "Electronica",
                    "Importe" : 400
                }, 
                {
                    "Concepto" : "Alimentación",
                    "Importe" : 1000
                }
            ]
        }
    ]
}

Para que quede más claro lo vemos visualmente con Robomongo. Tenemos una ficha de una persona con sus atributos. La persona tiene una array de gastos. Cada gasto sus atributos y un array de compras.

3. Secuencia 1: Buscando solamente los documentos necesarios.

Para empezar vamos a cambiar un poco el modo de escribir los comandos: la coma la vamos a poner al principio porque, de ese modo, podemos comentar las lineas siguientes e ir viendo cada línea del lote qué es lo que hace.

Lo lógico en una cadena es restringir al máximo el conjunto de documentos que recuperaremos.

match nos permite definir los datos sobre los que filtrar.

4. Secuencia 2: Elevando los gastos a primer nivel.

Como podemos observar, los gastos son un array, cosa que complica las operaciones de agrupación. Vamos a usar el comando $unwind para crear duplicados del documento pero subiendo cada elemento del array a un objeto.

5. Secuencia 3: Elevando las compras a primer nivel.

Ahora vamos a repetir operación y vamos a convertir cada elemento del array de Compra a una nueva copia de documento.

Acabamos por tanto realizar una labor de "multiplicación" de los documentos.

6. Secuencia 4: Filtrando los campos y colocándolos para manipularlos con facilidad.

Podemos ver que los documentos tienen muchos datos y podemos restringir los que nos interesan y simplificar la estructura del documento a procesar.

7. Secuencia Final : Agrupamiento.

Con ya los documentos necesarios y con estructura sencilla hacemos la función de agrupamiento.

Creamos una clave compuesta formada por la fecha, comercio y concepto: ,{ $group: { _id : { fecha : "$fecha" , comercio : "$comercio", concepto : "$concepto" } , importe : { $sum : "$importe" } } }

db.roberto.aggregate(
     {  $match: {  sede : "Madrid"        }    }
     ,{ $unwind : "$gastos"  }
     ,{ $unwind : "$gastos.Compra" }

    ,{ $project : { centro      : "$sede" ,  
            fecha       : "$gastos.fecha", 
            comercio    : "$gastos.Comercio", 
            concepto    : "$gastos.Compra.Concepto",  
            importe     : "$gastos.Compra.Importe"  }
     }              

    ,{  $group: {  _id : {  fecha : "$fecha" , comercio : "$comercio", concepto : "$concepto" } , importe : { $sum : "$importe" }    }    }
)

Cambiando simplemente el orden de los campos de la clave podemos obtener las agrupaciones deseadas.

8. Mejorando la secuencia.

Curiosamente, tendremos que plantearnos pronto que la gracia del BigData es usar millones de datos y que, por tanto, hay que ser cuidadoso en cómo se hacen las cosas.

Hay limitaciones en cuando al uso de recursos así que tenemos que ver si lo mismo se puede hacer en menos pasos o con menos datos.

Podemos pronto ver que la projection no era imprescindible y que podíamos acceder a los campos directamente.

db.roberto.aggregate(
     {  $match: {  sede : "Madrid"        }    }
     ,{ $unwind : "$gastos"  }
     ,{ $unwind : "$gastos.Compra" }

     ,{  $group: {  
        _id :   {  fecha : "$gastos.fecha" , comercio : "$gastos.Comercio", concepto : "$gastos.Compra.Concepto" } , 
        importe : { $sum : "$gastos.Compra.Importe" }    }    }     
)

9. Dudas sobre otras optimizaciones.

Otra cosa que nos podríamos plantear es hacer una proyección previa para eliminar todos los datos que no son necesarios, del documento original, en el resto de la cadena. Parecería sensato si el documento tiene muchos campos no usados.

db.roberto.aggregate(
     {  $match: {  sede : "Madrid"              }    }
     ,{ $project : { sede      : "$sede" ,  gastos    : "$gastos" }  }

     ,{ $unwind : "$gastos"  }
     ,{ $unwind : "$gastos.Compra" }

     ,{  $group: {  
        _id :   {  fecha : "$gastos.fecha" , comercio : "$gastos.Comercio", concepto : "$gastos.Compra.Concepto" } , 
        importe : { $sum : "$gastos.Compra.Importe" }    }    }     
)

10. Almacenamiento de los datos de una agregación en otra colección.

Una de las cosas que seguro que nos interesa hacer es almacenar el resultado de una agregación en una nueva colección. Esto es trivial:

resultado = db.roberto.aggregate(
     {  $match: {  sede : "Madrid"              }    }
     ,{ $project : { sede      : "$sede" ,  gastos    : "$gastos" }  }

     ,{ $unwind : "$gastos"  }
     ,{ $unwind : "$gastos.Compra" }

     ,{  $group: {  
        _id :   {  fecha : "$gastos.fecha" , comercio : "$gastos.Comercio", concepto : "$gastos.Compra.Concepto" } , 
        importe : { $sum : "$gastos.Compra.Importe" }    }    }     
)
db.datawh.insert(resultado.result);

En sistemas reales deberíamos diferenciar lo que siempre se ha llamado de Rabioso OnLine (ahora near real time) del OnLine. Esto significa que aunque tengamos un flujo de millones de datos de entrada en un sistema por día (near real time), es posible que para tomar decisiones de negocio lo que nos interese sean las tendencias comparativas con ciclos similares anteriores (consultas a datos OnLine resumen de históricos recientes). Por tanto, cobra mucho sentido el ir procesando grupos de datos y almacenándolos ya agrupados en colecciones auxiliares.

Incluso parece interesante montar distintos motores de base de datos NoSQL, en cascada, donde cada una de ellas esté especializada en una cosa. Una primera base de datos optimizada en escritura podría valernos para almacenar millones de registros. Consultando y extrayendo datos de un modo programado (por rangos de horas o días) se podría almacenar en otra optimizada en esquemas más complejos o lectura, para facilitar el análisis de datos.

Funcionalidades añadida en otras versiones

Esta funcionalidad de inserción del resultado en una agregación ya viene en la versión 2.6 de MongoDB añadiendo un comando out a la agregación, como se hace ya con MapReduce.

11. Conclusiones.

Como podemos observar, cerramos una cereza pero abrimos un melón: tenemos que saber el impacto interno de lo que hacemos porque podríamos saturar el servidor/es por malas decisiones en el procesamiento de datos.

Los siguiente estudios que debería hacer serían de rendimiento con millones de datos para ver si el efecto de las optimizaciones son tal cual sospechamos.

Siempre se ha dicho: construye los sistemas para que sean mantenibles y luego optimizarlo. Cuando tenemos millones de datos en juego, lo mismo tenemos que reformular el dicho 😉

2 COMENTARIOS

  1. Es estoy usando Hibernate y JPA con MongoDB, mi duda es ¿Es recomendables usar una sequencia o counter para generar el ID o es mejor usar el ObjectId que genera MongoDB? ¿El usar secuencias alenta a Mongo? ¿Podemos correr el riesgo de tener un id duplicado con la secuenci?

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad