Series temporales
Los datos almacenados como series temporales se caracterizan por su secuencia cronológica y alta frecuencia de creación. Las series temporales se emplean principalmente para la predicción y análisis de tendencias en muchos servicios financieros y aplicaciones IoT.
Desde la versión 5.0, MongoDB permite crear un tipo de colección específica para este tipo de datos. Estas colecciones time series definen una estructura y emplean una serie de índices que reducen el espacio necesario y aceleran las consultas que se realizan sobre los datos.
Las colecciones de series temporales se crean de forma explícita, y no podemos convertir una colección ordinaria a serie temporal. Eso sí, podemos programar un script para migrar los datos de una colección normal a una serie temporal.
Creando una serie temporal¶
A la hora de crear la colección, necesitamos indicar los siguiente campos:
timeField
: campo obligatorio que almacena el timestamp del dato. Debe ser un objetoDate()
.metaField
: campo opcional que indica dónde se almacenan los metadatos que describen la serie. Este campo puede contener un documento embebido o el dato único de valor, y normalmente identifica al sensor. Debe ser un valor que raramente cambie de valor y se puede utilizar como la segunda dimensión para filtrar los datos (considerando que la primera será el tiempo). Este campometaField
va a servir para particionar los datos, de manera que aquellos documentos que tengan el mismometaField
estarán en la misma partición.granularity
: campo opcional que ayuda a MongoDB a optimizar el almacenamiento al definir la frecuencia de los datos. Sus posibles valores sonseconds
(valor por defecto que se aplica si no indicamos la granularidad),minutes
ohours
y debe configurarse al valor más cercano entre dos lectura consecutivas de datos. Por ejemplo, si realizásemos una lectura del sensor cada cinco minutos, la granularidad debería serminutes
.expireAfterSeconds
: campo opcional, indicando la cantidad de segundos a partir de la creación del dato que MongoDB esperará para borrarlo automáticamente.
Por ejemplo, vamos a crear una colección llamada serietemporal
indicando el campo donde estará el timestamp y el de los metadatos:
db.createCollection( "serietemporal", {
timeseries: {
timeField: "timestamp",
metaField: "metadata"
}
})
A continuación, ya podemos añadirle datos:
db.serietemporal.insertMany( [
{
"metadata": {
"sensorId": 1234,
"type": "temperatura"
},
"timestamp": ISODate("2023-11-07T00:00:00.000Z"),
"temp": 16
},
{
"metadata": {
"sensorId": 1234,
"type": "temperatura"
},
"timestamp": ISODate("2023-11-07T04:00:00.000Z"),
"temp": 14
},
{
"metadata": {
"sensorId": 1234,
"type": "temperatura"
},
"timestamp": ISODate("2023-11-07T08:00:00.000Z"),
"temp": 13
}
])
Y obtener los datos como en una colección normal:
db.serietemporal.findOne({ "timestamp": ISODate("2023-11-07T00:00:00.000Z") })
Obteniendo el documento:
{
timestamp: 2023-11-07T00:00:00.000Z,
metadata: {
sensorId: 1234,
type: 'temperatura'
},
temp: 16,
_id: ObjectId("654a35e782c4b36cdaf13b6f")
}
Si visualizamos la colección desde MongoDB Compass o MongoAtlas veremos que aparece una pequeña leyenda informando que la colección es de tipo time-series:
Modelos de ejemplo¶
A la hora de modelar los datos, algunos modelos de datos de ejemplo posibles podrían ser:
{
"ts" : ISODate("2021-05-20T10:24:51.303Z"),
"metadata" : {
"sensorId": 123,
"region": "Alicante"
},
"presionAire" : 99 ,
"velocidadViento" : 22,
"temperatura" : {
"gradosF": 39,
"gradosC": 3.8
},
}
{
_id: ObjectId("6166df318f32e5d3ed304fc7"),
timestamp: ISODate("2023-10-13T00:00:00.000Z"),
metadata: {
nombre: 'AMZN',
moneda: 'Dolar'
},
precio: 142.83
},
{
_id: ObjectId("6166df318f32e5d3ed304fc5"),
timestamp: ISODate("2023-10-13T00:00:00.000Z"),
metadata: {
nombre: 'AAPL',
moneda: 'Dolar'
},
precio: 189.71
},
Consideraciones¶
Las colecciones de series temporales crean y utilizan un índice interno. Además, si fuera necesario, podemos crear índices adicionales, a los que se les conoce como índices secundarios.
Las colecciones de series temporales se asocian a casos de uso donde los datos se insertan una vez, se leen muchas veces y raramente se actualizan.
Es por ello, que las operaciones de modificación y borrado en las series temporales necesitan del operador {multi:true}
y {justOne:false}
para aplicarse sobre múltiples documentos, y la consulta a modificar o borrar sólo debe hacerlo sobre los campos del subdocumento del atributo metaField
. Así pues, sólo podemos modificar el subdocumento del campo metaField
y no podemos emplear una operación upsert
.
Cuidado
El tamaño máximo de un documento en una serie temporal es de 4MB, no los 16 de un documento ordinario.
Respecto al particionado, podemos emplear timeField
o metaField
con claves. Si utilizamos el timeField
, al tratarse de un timestamp, todas las escrituras irán a la última partición. Por lo tanto, es una buena práctica combinar el timeField
con uno o más campos del metaField
.
Migrando datos¶
En los datos de ejemplo cargados en MongoDB, tenemos una colección con datos climáticos. Por ejemplo, uno de los documentos de sample_weatherdata.data
es similar a:
{
"_id": {
"$oid": "5553a998e4b02cf7151190b8"
},
"st": "x+47600-047900",
"ts": {
"$date": "1984-03-05T13:00:00.000Z"
},
"position": {
"type": "Point",
"coordinates": [
-47.9,
47.6
]
},
"elevation": 9999,
"callLetters": "VCSZ",
"qualityControlProcess": "V020",
"dataSource": "4",
"type": "FM-13",
"airTemperature": {
"value": -3.1,
"quality": "1"
},
"dewPoint": {
"value": 999.9,
"quality": "9"
},
"pressure": {
"value": 1015.3,
"quality": "1"
},
"wind": {
"direction": {
"angle": 999,
"quality": "9"
},
"type": "9",
"speed": {
"rate": 999.9,
"quality": "9"
}
},
"visibility": {
"distance": {
"value": 999999,
"quality": "9"
},
"variability": {
"value": "N",
"quality": "9"
}
},
"skyCondition": {
"ceilingHeight": {
"value": 99999,
"quality": "9",
"determination": "9"
},
"cavok": "N"
},
"sections": [
"AG1"
],
"precipitationEstimatedObservation": {
"discrepancy": "2",
"estimatedWaterDepth": 999
}
}
Por ejemplo, si quisiéramos trabajar con los datos de la temperatura del aire y colocarlos dentro de una serie temporal, el primero paso sería crear la colección
use iabd
db.createCollection( "ts_weather", {
timeseries: {
timeField: "timestamp",
metaField: "metadata"
}
})
Y a continuación, mediante el framework de agregación, migramos los datos que nos interesen. Por ejemplo, nos vamos a traer la temperatura del aire y la presión, así como algunos metadatos del sensor para aquellas muestras que no han superado los 100 grados de temperatura (así podríamos filtrar algunos outliers):
A partir de la versión 7.0.3 podemos emplear el operador $out
para exportar los datos a una colección de tipo time series:
use sample_weatherdata
db.data.aggregate([
{ $match: {
"airTemperature.value": {$lt: 100}
}
},
{ $project: {
"timestamp": "$ts",
"metadata": {
"posicion" : "$position",
"elevacion" : "$elevation",
"tipo": "$type"
},
"temp_aire": "$airTemperature.value",
"presion": "$pressure.value"
}
},
{ $out: {
db: "iabd",
coll: "ts_weather",
timeseries: {
timeField: "timestamp",
metaField: "metadata"
}
}
}
])
Si no, primero crearemos una colección únicamente con los datos deseados:
use sample_weatherdata
db.data.aggregate([
{ $match: {
"airTemperature.value": {$lt: 100}
}
},
{ $project: {
"timestamp": "$ts",
"metadata": {
"posicion" : "$position",
"elevacion" : "$elevation",
"tipo": "$type"
},
"temp_aire": "$airTemperature.value",
"presion": "$pressure.value"
}
},
{ $out: "ts_weather"}
])
Y a continuación, debemos exportar los datos y volver a importarlos. Así pues, exportamos mediante mongodump
desde sample_weatherdata.ts_weather
:
mongodump --uri="mongodb+srv://usuario:password@host/sample_weatherdata" \
--collection=ts_weather
e importamos con mongorestore
en iabd.ts_weather
(que es la colección de tipo serie temporal que hemos creado en un paso anterior):
mongorestore --uri="mongodb+srv://usuario:password@host/iabd" \
--collection=ts_weather --noIndexRestore --maintainInsertionOrder \
dump/sample_weatherdata/ts_weather.bson
Si cambiamos a la base de datos de iabd
, podemos observar cómo se ha rellenado la colección de la serie temporal:
> use iabd
< switched to db iabd
> db.ts_weather.findOne()
< {
timestamp: 1984-03-05T12:00:00.000Z,
metadata: {
elevacion: 9999,
posicion: {
coordinates: [
14,
36.6
],
type: 'Point'
},
tipo: 'FM-13'
},
_id: ObjectId("5553a998e4b02cf715119936"),
temp_aire: 13,
presion: 1008.5
}
Agregando datos¶
Otros operadores de fecha
Además de $dateToParts
, podemos emplear los operadores $hour, $week, $dayOfYear, $year, etc... sobre un campo de tipo fecha / timestamp.
Es muy normal agrupar los datos en rangos de fechas sobre los que realizaremos cálculos. Para ello, podemos utilizar el operador $dateToParts
el cual recibe un campo date
con un valor de tipo fecha/timestamp, y crea un documento con los diferentes campos por separado (año, mes, día, etc... ) mediante las propiedades year
, month
, day
, hour
, minute
, second
y millisecond
. Además, si lo necesitamos, podemos configurar también su timezone.
Por ejemplo, sobre la colección recién importada, vamos a agrupar los datos por días y calcularemos la temperatura media del aire:
db.ts_weather.aggregate( [
{
$project: {
date: {
$dateToParts: { date: "$timestamp" }
},
temp_aire: 1
}
},
{
$group: {
_id: {
date: {
year: "$date.year",
month: "$date.month",
day: "$date.day"
}
},
avgTmp: { $avg: "$temp_aire" }
}
}
] )
Obteniendo como resultado documentos similares a:
{
_id: {
date: {
year: 1984,
month: 3,
day: 10
}
},
avgTmp: 68.94788047255038
},
{
_id: {
date: {
year: 1984,
month: 3,
day: 12
}
},
avgTmp: 65.79275964391691
}
...
Creando un gráfico¶
Si estamos trabajando con MongoAtlas, podemos hacer uso de Graphs y crear diferentes gráficos a modo de cuadro de mandos sobre la información almacenada en nuestras colecciones.
Más información en https://www.mongodb.com/docs/charts/
Referencias¶
- Artículo sobre MongoDB Time Series Data
- Píldora sobre Creating a Time Series Collection
- Series temporales en Practical MongoDB Aggregation Book