La gestión eficiente de datos de series temporales a escala de hyperscaler no es una cuestión de elegir la base de datos "correcta", sino de aplicar principios fundamentales de diseño de almacenamiento y acceso a datos. El problema central radica en la redundancia inherente de los metadatos de la serie (dimensiones) y la naturaleza temporalmente localizada de las escrituras y lecturas. Este artículo aborda cómo la normalización de la identidad de la serie, el aprovechamiento del almacenamiento columnar y la implementación de estrategias de particionamiento multidimensional resuelven estos desafíos, reduciendo drásticamente el costo de almacenamiento y mejorando el rendimiento de las consultas. La relevancia de estas técnicas se ha intensificado con la proliferación de sensores y sistemas que generan volúmenes masivos de datos temporales, haciendo que la optimización a nivel de bytes sea crítica para la viabilidad económica y operativa.
Históricamente, las bases de datos relacionales no fueron diseñadas para la ingesta y consulta de series temporales de alta cardinalidad y alto volumen. La repetición de cadenas de dimensiones en cada fila de datos temporales conduce a una hinchazón masiva del almacenamiento y a un rendimiento deficiente de los índices. La evolución hacia bases de datos especializadas en series temporales y formatos de almacenamiento optimizados como Parquet, junto con capas de metadatos como Apache Iceberg, refleja la necesidad de abordar estos problemas desde los cimientos. La tesis es que, independientemente de la tecnología específica, la aplicación de estos principios de diseño es lo que realmente determina la eficiencia y escalabilidad de un sistema de series temporales.
Arquitectura del Sistema
La arquitectura propuesta para la gestión de series temporales se basa en una combinación de esquemas relacionales normalizados para la identidad de la serie y almacenamiento columnar para los datos de medición. En un esquema relacional como PostgreSQL, se utiliza una tabla series_dim para almacenar las dimensiones (tags) de cada serie, referenciada por un series_id entero en la tabla de mediciones readings_normalized. Esto reduce la redundancia de cadenas de texto en cada fila de datos temporales. Para la evolución del esquema, las dimensiones se almacenan como jsonb en series_dim, permitiendo flexibilidad sin migraciones de esquema, con índices GIN para búsquedas generales y B-tree expression indexes para atributos calientes.
Para el almacenamiento a largo plazo y el análisis de datos fríos, se adopta un enfoque columnar utilizando Apache Parquet en object storage (ej., S3). Parquet, un formato de archivo, no una base de datos, se complementa con motores de consulta como DuckDB, AWS Athena, Trino, Presto o Apache Spark. Para añadir capacidades ACID, evolución de esquema y gestión de particiones, se utiliza Apache Iceberg como una capa de metadatos sobre los archivos Parquet. Iceberg permite la partición oculta por tiempo y otras dimensiones, aislamiento de snapshots y compactación automática. La ingesta a escala en Iceberg se realiza mediante Spark Structured Streaming o Apache Flink. El particionamiento es bidimensional: primero por tiempo (ej., diario) y luego por una dimensión espacial (ej., hash de series_id) para distribuir las escrituras y lecturas, mitigando los hotspots de escritura en la partición actual. El downsampling reduce la resolución de los datos a medida que envejecen, almacenando agregados precalculados en intervalos más gruesos para optimizar el costo y el rendimiento de las consultas históricas.
Flujo de Ingesta y Almacenamiento de Series Temporales
- 1 Generación de Datos Sensores/Sistemas emiten mediciones con timestamp, dimensiones y métricas.
- 2 Normalización de Identidad Las dimensiones se mapean a un 'series_id' único en 'series_dim' (PostgreSQL).
- 3 Ingesta en Capa Caliente Datos de mediciones con 'series_id' y timestamp se escriben en 'readings_norm...
- 4 Particionamiento Las escrituras se distribuyen en particiones por tiempo y hash de 'series_id'.
- 5 Exportación/Streaming a Capa Fría Datos de la capa caliente se exportan periódicamente o se transmiten a Parque...
- 6 Almacenamiento Columnar Datos se guardan en archivos Parquet, gestionados por Apache Iceberg en Objec...
| Capa | Tecnología | Justificación |
|---|---|---|
| storage | PostgreSQL | Base de datos relacional para la capa caliente (ingesta reciente, consultas de baja latencia) y para almacenar la tabla de dimensiones normalizadas (series_dim). vs MySQL, Oracle, SQL Server Uso de 'jsonb' para dimensiones flexibles y particionamiento declarativo por rango (tiempo y series_id). |
| storage | Apache Parquet | Formato de archivo columnar para almacenamiento eficiente de datos de series temporales en la capa fría, optimizado para compresión y consultas analíticas. vs Apache ORC, CSV, JSON Uso de dictionary encoding y otras compresiones columnares para reducir el tamaño. |
| storage | Apache Iceberg | Capa de tabla abierta sobre Parquet para añadir características ACID, evolución de esquema, particionamiento oculto y gestión de snapshots en object storage. vs Delta Lake, Apache Hudi Integración con Spark Structured Streaming/Apache Flink para ingesta y con múltiples motores de consulta. |
| storage | Amazon S3 | Object storage escalable y de bajo costo para almacenar archivos Parquet/Iceberg, desacoplando el costo de almacenamiento del cómputo. vs Google Cloud Storage (GCS), MinIO, Azure Blob Storage |
| data-processing | Apache Spark Structured Streaming | Motor de procesamiento de streams para la ingesta de datos en tiempo real directamente en tablas Iceberg, manejando particionamiento y tamaño de archivos. vs Apache Flink, Kafka Streams Soporte nativo para sinks de Iceberg. |
| data-processing | DuckDB | Base de datos analítica embebida para análisis single-node y exploración de archivos Parquet directamente desde S3. vs SQLite (para OLTP), Pandas (para análisis en memoria) |
Trade-offs
Ganancias
- ▲ Reducción de almacenamiento
- ▲ Flexibilidad de esquema
- ▲ Rendimiento de consulta (agregaciones)
- ▲ Gestión de retención de datos
- ▲▲ Costo de almacenamiento (capa fría)
Costes
- ▲ Complejidad de la arquitectura (múltiples sistemas)
- △ Costo de escritura (índices GIN en jsonb)
- ▲ Gestión de tamaño de archivos Parquet
- △ Latencia de consulta (capa fría)
CREATE TABLE series_dim (
series_id serial PRIMARY KEY,
device_id text NOT NULL,
location text NOT NULL,
region text NOT NULL,
UNIQUE (device_id, location, region)
);
CREATE TABLE readings_normalized (
series_id integer REFERENCES series_dim(series_id),
ts timestamptz NOT NULL,
metric_name text NOT NULL,
value double precision NOT NULL
);
CREATE INDEX idx_norm_series_ts ON readings_normalized (series_id, ts);CREATE TABLE series_dim (
series_id bigserial PRIMARY KEY,
dimensions jsonb NOT NULL
);CREATE TABLE readings (
series_id integer NOT NULL,
ts timestamptz NOT NULL,
metric_name text NOT NULL,
value double precision NOT NULL
) PARTITION BY RANGE (ts);
CREATE TABLE readings_2026_02_01
PARTITION OF readings
FOR VALUES FROM ('2026-02-01') TO ('2026-02-02');Fundamentos Teóricos
El concepto de normalización de esquemas, fundamental para reducir la redundancia y mejorar la integridad de los datos, se remonta a los trabajos de Edgar F. Codd en la década de 1970 sobre el modelo relacional. Su teoría de las formas normales (1NF, 2NF, 3NF, BCNF) proporciona la base teórica para la separación de la identidad de la serie en una tabla series_dim, minimizando la repetición de atributos. Aunque el artículo no cita directamente a Codd, la aplicación de la normalización es una manifestación directa de estos principios.
La discusión sobre el almacenamiento columnar se conecta con la investigación en bases de datos analíticas y data warehousing. El paper "Column-stores vs. Row-stores: How Different Are They Really?" (Abadi et al., SIGMOD 2008) es una referencia clave que explora las ventajas de los almacenes columnares para cargas de trabajo analíticas, particularmente en la reducción de I/O y la eficiencia de compresión. La aplicación de técnicas como la codificación por diccionario (dictionary encoding) y la codificación de longitud de ejecución (RLE) en formatos como Parquet para series temporales es una implementación práctica de estos principios de compresión y eficiencia de consulta que se estudian en la academia. El particionamiento bidimensional para distribuir la carga de escritura y lectura en sistemas distribuidos se relaciona con los principios de diseño de bases de datos distribuidas y sistemas de archivos distribuidos, buscando optimizar la localidad de los datos y evitar hotspots, un problema fundamental en la computación distribuida.