Guía TSDB: InfluxDB, TimescaleDB y ClickHouse
Guía práctica de TSDB: cuándo usar una base de datos de series temporales, modelo de datos, retención y cómo elegir entre InfluxDB, TimescaleDB y ClickHouse.
Descripción General
Las bases de datos de series temporales (TSDB) están optimizadas para cargas de trabajo donde los datos se indexan principalmente por tiempo: métricas, lecturas de sensores IoT, logs de aplicaciones, ticks financieros y monitorización de sistemas. A diferencia de las bases de datos de propósito general, las TSDB aprovechan la naturaleza inmutable y de solo-adición de las series temporales para lograr una ingesta masiva y consultas eficientes por rango temporal. Motores especializados como InfluxDB, TimescaleDB y ClickHouse equilibran de forma distinta la facilidad de uso, la compatibilidad con SQL y el rendimiento bruto.
Cuándo Usar
- Ingestas millones de puntos de datos por segundo, cada uno con una marca de tiempo
- Las consultas son predominantemente escaneos por rango temporal (
WHERE time > now() - interval '1 day') - Los datos son de solo-adición y rara vez se actualizan tras la inserción
- Necesitas políticas de retención y downsampling para controlar el crecimiento del almacenamiento
- Las consultas de agregación (avg, sum, count) sobre ventanas deslizantes son habituales
No uses una TSDB cuando los registros se actualizan en el sitio, cuando importan los joins y la integridad referencial, o cuando unos pocos miles de filas cubren tu histórico — PostgreSQL a secas resuelve eso mejor.
Comparación
| Característica | InfluxDB | TimescaleDB | ClickHouse |
|---|---|---|---|
| Motor base | Propio (TSM/TSI) | Extensión de PostgreSQL | OLAP columnar |
| Soporte SQL | InfluxQL / Flux | SQL completo | Dialecto SQL |
| Facilidad de instalación | Binario único | PostgreSQL + extensión | Más complejo |
| Tasa de ingesta | Muy alta | Alta | Extremadamente alta |
| Compresión | Buena | Buena | Excelente |
| Ideal para | Métricas DevOps, IoT | Apps ya sobre Postgres | Analítica, big data |
Dos vecinas que conviene conocer: VictoriaMetrics apunta al nicho compatible con Prometheus (remote storage directo, ingesta rápida y operación más simple que una TSDB general) y QuestDB habla line protocol mientras responde SQL a velocidad de ClickHouse. Los tres motores de arriba siguen siendo los que con más probabilidad heredes o evalúes primero.
Modelo de Datos (InfluxDB Line Protocol)
measurement,tag1=value1,tag2=value2 field1=42.0,field2="text" 1465839830100400200
- Measurement: contenedor lógico (como una tabla)
- Tags: dimensiones de metadatos indexadas (ID de dispositivo, región) — caras con alta cardinalidad
- Fields: los valores reales (temperatura, uso de CPU) — no indexados
- Timestamp: precisión de nanosegundos (a menudo más de lo que necesitas)
Ejemplo con TimescaleDB
-- Habilitar la extensión TimescaleDB y crear la hypertable
CREATE EXTENSION IF NOT EXISTS timescaledb;
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL,
sensor_id INT NOT NULL,
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION
);
-- Convertir a hypertable con chunks de 1 día
SELECT create_hypertable('sensor_data', by_range('time', INTERVAL '1 day'));
-- Agregado continuo para promedios horarios
CREATE MATERIALIZED VIEW hourly_avg
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) as bucket,
sensor_id,
AVG(temperature) as avg_temp,
AVG(humidity) as avg_humidity
FROM sensor_data
GROUP BY bucket, sensor_id;
-- Política de retención: eliminar chunks de más de 90 días
SELECT add_retention_policy('sensor_data', INTERVAL '90 days');
Ejemplo con ClickHouse
-- Crear tabla MergeTree para datos de series temporales
CREATE TABLE events (
event_time DateTime,
user_id UInt64,
event_type String,
value Float64
) ENGINE = MergeTree()
ORDER BY (event_type, event_time);
-- Consulta eficiente por rango temporal
SELECT
toStartOfHour(event_time) as hour,
count() as event_count,
avg(value) as avg_value
FROM events
WHERE event_time > now() - INTERVAL 7 DAY
GROUP BY hour
ORDER BY hour;
La clave ORDER BY es la decisión más importante en ClickHouse: los datos se ordenan físicamente por ella, así que las consultas que filtran por las columnas iniciales se saltan la mayor parte de la tabla. Pon primero el filtro selectivo (event_type arriba) y el tiempo después.
Ingesta a Escala
La forma más rápida de romper una TSDB es insertar filas de una en una. Todos los motores quieren lotes grandes y ordenados — lo que cambia es el tamaño adecuado y cómo los entregas.
| Motor | Vía de escritura | Tamaño de lote | Notas |
|---|---|---|---|
| InfluxDB | Line protocol sobre HTTP | 5K–10K líneas | Activa gzip; agrupa por measurement para amortizar el parseo |
| TimescaleDB | INSERT / COPY | 10K–50K filas por transacción | COPY es la vía rápida para backfills; un commit por lote, nunca por fila |
| ClickHouse | Insert nativo o HTTP | 10K–1M filas | Los inserts pequeños crean parts diminutas — los merges se atrasan hasta el “too many parts” |
Dos reglas aplican en todos:
- Bufferiza en el cliente y flushea por tamaño o por tiempo. Un colector que escribe cada 10K puntos o 5 segundos mantiene la vía de escritura caliente sin frenar a los productores.
- Mantén timestamps monótonos dentro del lote. Los puntos fuera de orden se aceptan, pero obligan al motor a fusionar en chunks o parts antiguos — tolerable con moderación, caro como estado estable.
Si ingieres a través de Kafka o una cola, agrupa en el consumidor, no en el productor: la cola absorbe las ráfagas para que la base de datos vea escrituras constantes y bien dimensionadas.
ClickHouse guarda aquí un as bajo la manga: async_insert=1 bufferiza inserts pequeños en el servidor y los vuelca como parts de tamaño correcto, lo que te ahorra escribir la capa de batching para cargas moderadas. En TimescaleDB, alinea también el chunk_time_interval de la hypertable con tu ritmo de escritura — rotar chunks medio vacíos gasta la misma memoria que intentas ahorrar del lado de lectura. Y cuando los lotes siguen llegando desordenados a escala, una mirada a system.parts te dice si los merges van al día antes de que lo haga la latencia.
La Misma Consulta en Tres Motores
Pide la media horaria por sensor de las últimas 24 horas y cada motor responde en su propio dialecto:
-- TimescaleDB: time_bucket hace el agrupado
SELECT time_bucket('1 hour', time) AS hour, sensor_id, avg(temperature)
FROM sensor_data
WHERE time > now() - INTERVAL '24 hours'
GROUP BY hour, sensor_id;
-- ClickHouse: toStartOfHour es el equivalente
SELECT toStartOfHour(event_time) AS hour, user_id, avg(value)
FROM events
WHERE event_time > now() - INTERVAL 24 HOUR
GROUP BY hour, user_id;
-- InfluxDB: InfluxQL agrupa por intervalo de forma nativa
SELECT mean("temperature") FROM "sensor_data"
WHERE time > now() - 24h
GROUP BY time(1h), "sensor_id";
La segunda consulta que escribirás constantemente — el último valor por serie — muestra aún mejor la personalidad de cada uno:
-- TimescaleDB: DISTINCT ON, el idioma de Postgres
SELECT DISTINCT ON (sensor_id) sensor_id, time, temperature
FROM sensor_data
ORDER BY sensor_id, time DESC;
-- ClickHouse: argMax lo hace en una pasada
SELECT user_id, argMax(value, event_time) AS latest
FROM events
GROUP BY user_id;
InfluxDB la resuelve gratis: SELECT last("temperature") FROM "sensor_data" GROUP BY "sensor_id". DISTINCT ON es elegante con pocas series pero escanea más a medida que crece la cardinalidad; argMax se mantiene plano porque la clave de ordenación hace el trabajo. Más allá de estos idiomas, el optimización de consultas SQL de siempre sigue aplicando — la poda de chunks y los índices normales hacen la mayor parte del trabajo en TimescaleDB.
Retención y Downsampling
| Estrategia | Cómo | Compromiso |
|---|---|---|
| Retención raw | Mantener todos los datos N días | Máxima fidelidad, máximo coste |
| Downsampling | Agregar a menor resolución tras N días | Ahorra almacenamiento, pierde granularidad |
| Almacenamiento por niveles | Mover chunks antiguos a almacenamiento frío | Consultas antiguas más lentas, más barato |
-- InfluxDB: consulta continua para downsampling
CREATE CONTINUOUS QUERY "hourly_cpu" ON "monitoring"
BEGIN
SELECT mean("usage") INTO "downsampled"."autogen"."cpu_1h"
FROM "monitoring"."autogen"."cpu"
GROUP BY time(1h),*
END;
Un pipeline típico conserva los datos raw calientes unos días, agregados por minuto durante meses y agregados por hora durante años — el mismo compromiso de pre-agregación que un pipeline de métricas toma antes de llegar a la base de datos:
La compresión es donde los motores se pagan solos sin hacer ruido. TimescaleDB convierte los chunks antiguos a layout columnar con ALTER TABLE ... SET (timescaledb.compress) y suele reducirlos a una décima parte; ClickHouse permite elegir codecs por columna (Delta, DoubleDelta, Gorilla, ZSTD) y alcanza ratios similares con datos ordenados por tiempo; InfluxDB comprime sus bloques TSM automáticamente, sin knobs. También es la razón por la que “guardar raw para siempre” a veces resulta asequible — haz números con tus propios datos antes de asumir que necesitas downsampling agresivo.
La Cardinalidad Alta es el Verdadero Enemigo
Toda historia de fracaso con una TSDB empieza igual: alguien mete un UUID, un ID de petición o un ID de usuario en un tag indexado. La cardinalidad — el número de combinaciones distintas de tags — determina el tamaño del índice y de la memoria. Diez sensores por tres métricas es trivial; diez millones de conjuntos de tags únicos pueden tumbar el motor.
Reglas que te mantienen a salvo:
- Los tags son para dimensiones por las que filtras o agrupas, con conjuntos de valores acotados: región, host, servicio, estado
- Nunca uses como tag IDs de petición, de sesión, de usuario, emails ni timestamps
- Los fields llevan los valores de alta cardinalidad — se almacenan, no se indexan
- Mide el número de series con regularidad (
influxdblo reporta; TimescaleDB lo aproxima vía estadísticas depg_classpor chunk)
Si necesitas consultas por usuario, esos datos van en una tabla relacional, no en el flujo de métricas.
Operar una TSDB en el Día a Día
Cuatro números merecen un dashboard propio:
- Tendencia de cardinalidad de series — la primera señal de un error de etiquetado; gráficala semanalmente
- Puntos ingeridos por segundo y lotes rechazados — los rechazos suben cuando los clientes mandan la precisión de timestamp equivocada o payloads demasiado grandes
- Uso de disco por nivel de retención — los datos raw deberían decaer de forma predecible; cuando no lo hacen, la retención está desalineada con el intervalo de chunk
- Retraso de los agregados continuos — cuando el refresco se atrasa, los dashboards sirven agregados obsoletos sin avisar
Conéctalos a tu pipeline de métricas como cualquier otro servicio — la guía de observabilidad cubre esa parte del setup.
Los backups siguen el grano del motor. TimescaleDB te mantiene dentro de PostgreSQL — pg_dump, archivado de WAL y recuperación a punto en el tiempo funcionan como siempre. InfluxDB trae sus propias herramientas de backup, y la edición open-source hace snapshots por base de datos, no del clúster entero. ClickHouse ofrece BACKUP ... TO Disk() además de clickhouse-backup para destinos S3 — prueba las restauraciones, porque un backup de MergeTree solo es tan fiable como los metadatos de sus parts.
Las actualizaciones también difieren: actualizar TimescaleDB es un ALTER EXTENSION timescaledb UPDATE más una comprobación de tus agregados continuos, mientras que un clúster de ClickHouse pide una actualización rodante, nodo a nodo, con la tabla Distributed absorbiendo el hueco.
Solución de Problemas
- La memoria explota tras añadir un tag: explosión de cardinalidad clásica — mira el recuento de valores distintos del tag, elimínalo y reconstruye el índice. En InfluxDB usa
SHOW SERIES CARDINALITY; en TimescaleDB revisa las estimaciones de filas por chunk. - Las consultas escanean muchos más datos de lo esperado: el intervalo de chunk está desalineado con las ventanas de consulta — una hypertable con chunks de 1 día obliga a escanear días completos para dashboards de 10 minutos. Recrea con un intervalo acorde a tu rango más común, o añade un agregado continuo.
- Los agregados continuos se atrasan: la ventana de la política de refresco es demasiado corta para las ráfagas de escritura, o un backfill por lotes cayó fuera del intervalo de refresco. Amplía la ventana o refresca explícitamente con
CALL refresh_continuous_aggregate(...). - Puntos descartados en silencio durante la ingesta: line protocol mal formado, timestamps en la unidad equivocada (segundos vs nanosegundos) o lotes demasiado grandes. Registra los lotes rechazados y valida la precisión antes de culpar al motor.
- La política de retención no libera disco: la retención elimina chunks enteros, no filas — si el chunk contiene algún dato no expirado, sobrevive. Alinea el intervalo de chunk con el periodo de retención para que los chunks enteros expiren juntos.
Errores Comunes
- Usar una TSDB para cargas transaccionales — sin garantías ACID, sin updates, sin integridad referencial
- Tags de alta cardinalidad — demasiados valores únicos de tag disparan memoria y tamaño de índice (ver arriba)
- Sin política de retención — los datos temporales crecen indefinidamente; define siempre retención o archivado
- Precisión de timestamp equivocada — la precisión de nanosegundos suele ser excesiva y desperdicia almacenamiento
- Backfill por la vía caliente — las cargas históricas masivas necesitan lotes y (en TimescaleDB) una ventana de refresco relajada temporalmente
- Copiar una tabla comparativa como decisión — el motor correcto depende de tu stack actual, no de los titulares de los benchmarks
Temas Avanzados
Escenario Detallado: Monitorización IoT con TimescaleDB
Un sistema que monitoriza 10.000 sensores IoT con TimescaleDB sobre PostgreSQL: 100.000 lecturas por segundo, 8.600 millones de lecturas al día, alertas en tiempo real, dashboards históricos y downsampling. Las versiones ejecutables de este setup están en el repositorio companion.
Esquema e hypertable — chunks de 6 horas alineados con las ventanas de los dashboards:
CREATE EXTENSION IF NOT EXISTS timescaledb;
CREATE TABLE sensor_readings (
time TIMESTAMPTZ NOT NULL,
sensor_id INT NOT NULL,
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
pressure DOUBLE PRECISION,
battery_level DOUBLE PRECISION
);
SELECT create_hypertable('sensor_readings', 'time',
chunk_time_interval => INTERVAL '6 hours');
CREATE INDEX idx_sensor_time ON sensor_readings (sensor_id, time DESC);
Ingesta simulada en ráfaga de 10.000 lecturas — alcanza unas 100K filas/seg en hardware commodity:
INSERT INTO sensor_readings (time, sensor_id, temperature, humidity, pressure)
SELECT
NOW() - (generate_series(1, 10000) * INTERVAL '1 second'),
(random() * 10000)::INT,
20 + random() * 15,
40 + random() * 20,
1013 + random() * 10;
Los agregados continuos gestionan los niveles de downsampling:
CREATE MATERIALIZED VIEW sensor_1m
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 minute', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temp,
MAX(temperature) AS max_temp,
MIN(temperature) AS min_temp,
AVG(humidity) AS avg_humidity
FROM sensor_readings
GROUP BY bucket, sensor_id
WITH NO DATA;
CREATE MATERIALIZED VIEW sensor_1h
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temp,
MAX(temperature) AS max_temp,
MIN(temperature) AS min_temp,
COUNT(*) AS sample_count
FROM sensor_readings
GROUP BY bucket, sensor_id
WITH NO DATA;
Retención por nivel — los datos raw expiran a la semana, los agregados por minuto cubren el trimestre y los horarios cubren la cola larga:
SELECT add_retention_policy('sensor_readings', INTERVAL '7 days');
SELECT add_retention_policy('sensor_1m', INTERVAL '90 days');
SELECT add_retention_policy('sensor_1h', INTERVAL '5 years');
Las consultas que corren sobre el sistema:
-- Última lectura de cada sensor
SELECT DISTINCT ON (sensor_id)
sensor_id, time, temperature, humidity
FROM sensor_readings
ORDER BY sensor_id, time DESC;
-- Alerta: sensores por encima de 35 C en los últimos 10 minutos
SELECT sensor_id, MAX(temperature) AS max_temp
FROM sensor_readings
WHERE time > NOW() - INTERVAL '10 minutes'
AND temperature > 35
GROUP BY sensor_id
ORDER BY max_temp DESC;
-- Tendencia de temperatura por hora de un sensor, últimas 24 horas
SELECT bucket, sensor_id, avg_temp, max_temp, min_temp
FROM sensor_1h
WHERE bucket > NOW() - INTERVAL '24 hours'
AND sensor_id = 42
ORDER BY bucket;
| Nivel | Datos | Retención | Tamaño |
|---|---|---|---|
| Raw | Todas las lecturas | 7 días | 50 GB |
| 1-min | Agregados por minuto | 90 días | 5 GB |
| 1-hora | Agregados por hora | 5 años | 1 GB |
Rendimiento medido sobre esta forma de datos:
| Consulta | Tiempo | Filas escaneadas |
|---|---|---|
| Última lectura (1 sensor) | 2 ms | 1 |
| Alertas en tiempo real (10 min) | 15 ms | ~6 M |
| Dashboard 24 h (1 sensor) | 8 ms | 24 (desde agregado) |
| Dashboard 30 días (todos) | 120 ms | 720 (desde agregado) |
Lecciones: TimescaleDB da ingesta de nivel TSDB sin salir de PostgreSQL, los agregados continuos eliminan los trabajos de pre-cómputo, la retención por niveles mantiene la factura de almacenamiento bajo control y chunk_time_interval tiene que alinearse con las ventanas de consulta que realmente ejecutas.
El escenario refleja el pipeline de retención del diagrama: los puntos raw expiran a la semana, los agregados por minuto cubren el trimestre y los agregados por hora cubren la cola larga — 56 GB de datos fuente comprimidos a ~1 GB de histórico consultable.
Escalar Más Allá de Un Nodo
Un solo nodo cubre la mayoría de cargas, pero ingestas de millones de puntos por segundo o retenciones en petabytes acaban obligando a un clúster — y cada motor escala de forma distinta:
- TimescaleDB sigue en un nodo: escala verticalmente, añade réplicas de lectura para la carga de consultas y usa almacenamiento por niveles para capacidad. La antigua función multi-nodo está deprecada en favor del particionado nativo de PostgreSQL más réplicas.
- InfluxDB gestiona el clustering en InfluxDB Cloud y Enterprise — el núcleo open-source corre en un solo nodo, así que el escalado horizontal pasa por la oferta gestionada o por una capa de colectores que distribuya la carga por measurement.
- ClickHouse escala de forma nativa:
ReplicatedMergeTreepara redundancia más tablasDistributedpara sharding entre nodos — la curva operativa más empinada de las tres, y la razón por la que los equipos lo adoptan solo cuando el volumen lo exige.
Si diseñas para un clúster desde el día uno, escribe el esquema de modo que particiones y shards se alineen con las dimensiones por las que realmente vas a fragmentar los datos — rehacer la clave de sharding después es doloroso.
Ver También
- Documentación de TimescaleDB — hypertables, agregados continuos, políticas de retención
- Documentación de InfluxDB — line protocol, Flux y comportamiento de retención
- Documentación de ClickHouse — motores MergeTree y particionado
- Guía Completa de Estrategias de Caché con Redis — cuándo una caché, no una TSDB, es la respuesta correcta
- Patrón de Agregación de Métricas — los compromisos de pre-agregación detrás del downsampling
Preguntas frecuentes
¿Puedo usar PostgreSQL para datos de series temporales?
Sí, con TimescaleDB. Para cargas pequeñas, PostgreSQL a secas con buenos índices funciona. Para tasas de ingesta altas, mejor una TSDB dedicada.
¿Debo guardar valores como tags o como fields?
Los tags están indexados; los fields no. Usa tags para las dimensiones por las que filtras o agrupas. Usa fields para los valores que agregas — y para cualquier cosa de cardinalidad no acotada.
¿Cómo hago backfill de datos históricos?
La mayoría de TSDB aceptan escrituras fuera de orden, pero el rendimiento se degrada. En TimescaleDB, amplía temporalmente la ventana de refresco del agregado continuo, inserta en lotes de ~100K filas y vuelve a habilitarla. En InfluxDB, usa el timePrecision correcto y evita tags de alta cardinalidad durante la carga. En ClickHouse, inserta en particiones alineadas por fecha para abaratar los merges.
¿Qué motor elijo para un equipo pequeño?
Si ya tienes PostgreSQL, empieza con TimescaleDB — mismas herramientas, mismos backups, mismo SQL. Elige InfluxDB cuando las métricas sean la única carga y quieras un binario único con un modelo de datos de convenciones fuertes. Ve a ClickHouse solo cuando el volumen analítico justifique el coste operativo.
¿Cómo mido si la TSDB funciona bien?
Mide throughput de ingesta, latencia de consulta en tus ventanas de dashboard reales, cardinalidad de series a lo largo del tiempo y uso de disco por nivel de retención. La cardinalidad creciendo semana a semana es la primera señal de que algo está mal etiquetado.
¿Puedo migrar de InfluxDB a TimescaleDB más adelante?
Sí, pero planea un periodo de doble escritura. Exporta vía line protocol o CSV, transforma los tags en columnas y reproduce en lotes. Los agregados continuos hay que reconstruirlos en TimescaleDB — no hay equivalente directo para los pipelines de Flux.
Recursos Relacionados
Patrones de Modelado NoSQL
Guía práctica de modelado NoSQL: embebido vs referenciado, diseño basado en patrones de acceso, y patrones para MongoDB, DynamoDB, Cassandra y Redis.
GuideGuía Completa de Optimización de Consultas SQL
Detecta y corrige consultas SQL lentas: lee planes EXPLAIN, indexa bien, elimina consultas N+1 y aplica particionamiento, pooling y caché en PostgreSQL y MySQL.
GuideGuía Completa de Observabilidad con el Stack de Grafana
Monta el stack completo de observabilidad de Grafana: Prometheus, Loki, Tempo, Alloy y Grafana con un solo docker-compose, correlación de señales y alertado por SLO.
PatternPatrón Metrics Aggregation
Cómo collect, tag, y aggregate business metrics para observability. Cubre Prometheus, OpenTelemetry, custom metrics, histograms, y dashboarding.