TimescaleDB: séries temporais sobre PostgreSQL sem abrir mão do SQL
1. Introdução ao TimescaleDB e o cenário de séries temporais
Dados de séries temporais são sequências de pontos indexados por tempo — métricas de servidor, leituras de sensores, cotações financeiras. Bancos relacionais tradicionais lidam mal com esse padrão: inserções constantes fragmentam índices, consultas agregadas por intervalo exigem scans completos, e o volume histórico cresce sem controle. Bancos especializados como InfluxDB e Prometheus surgiram para preencher essa lacuna, mas impõem uma barreira: abandonar o SQL e aprender novas linguagens de consulta.
TimescaleDB resolve esse dilema. Como extensão do PostgreSQL, ele transforma tabelas comuns em repositórios de séries temporais sem remover nenhum recurso SQL. Você mantém JOINs, subconsultas, funções de janela, transações ACID e todo o ecossistema de ferramentas PostgreSQL. A escolha ideal depende do cenário: InfluxDB é superior em ingestão pura de alta cardinalidade; Prometheus é excelente para monitoramento já integrado com Kubernetes. TimescaleDB brilha quando você precisa de flexibilidade analítica — consultas complexas que misturam dados temporais com relacionais, relatórios financeiros, ou integração com sistemas legados que já usam PostgreSQL.
2. Arquitetura fundamental: hypertables e chunks
O coração do TimescaleDB é a hypertable — uma abstração que faz uma tabela PostgreSQL comum se comportar como um banco de séries temporais. Internamente, a hypertable é particionada automaticamente em chunks: pedaços de dados organizados por tempo e, opcionalmente, por espaço (ex.: ID do sensor). Cada chunk é uma tabela PostgreSQL independente, permitindo paralelismo em consultas e manutenção individual.
Quando você insere dados, o TimescaleDB decide em qual chunk armazená-los com base no timestamp. Chunks antigos podem ser comprimidos com algoritmos nativos (redução de até 90% de armazenamento) ou descartados automaticamente via políticas de retenção. Tudo isso sem alterar a sintaxe SQL que você já conhece.
3. Instalação e primeiros passos com TimescaleDB
A instalação é direta via extensão PostgreSQL. No Docker:
docker run -d --name timescaledb -p 5432:5432 timescale/timescaledb:latest-pg16
Conecte-se e crie a extensão:
CREATE EXTENSION IF NOT EXISTS timescaledb;
Agora crie uma tabela comum para sensores IoT:
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL,
sensor_id INTEGER NOT NULL,
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION
);
Transforme-a em hypertable:
SELECT create_hypertable('sensor_data', 'time', chunk_time_interval => INTERVAL '1 day');
Insira dados de exemplo:
INSERT INTO sensor_data (time, sensor_id, temperature, humidity)
SELECT
generate_series('2024-01-01 00:00:00'::timestamptz, '2024-01-31 23:59:00'::timestamptz, '5 minutes'::interval),
floor(random() * 10 + 1)::int,
random() * 30 + 10,
random() * 50 + 30;
Pronto. Sua hypertable já está particionando automaticamente por dia.
4. Consultas poderosas com funções de séries temporais
TimescaleDB adiciona funções especializadas que simplificam análises temporais. A mais útil é time_bucket(), que agrupa dados em intervalos regulares:
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temp,
MAX(temperature) AS max_temp
FROM sensor_data
WHERE time >= NOW() - INTERVAL '7 days'
GROUP BY bucket, sensor_id
ORDER BY bucket;
Para janelas deslizantes, use first() e last() — que retornam o primeiro e último valor de um grupo sem precisar de subconsultas complexas:
SELECT
time_bucket('1 day', time) AS day,
sensor_id,
first(temperature, time) AS temp_at_start,
last(temperature, time) AS temp_at_end,
last(temperature, time) - first(temperature, time) AS delta_temp
FROM sensor_data
GROUP BY day, sensor_id;
Detecção de picos com função de janela:
SELECT
time,
temperature,
temperature - AVG(temperature) OVER (PARTITION BY sensor_id ORDER BY time ROWS BETWEEN 10 PRECEDING AND 10 FOLLOWING) AS anomaly_score
FROM sensor_data
WHERE sensor_id = 1 AND time >= NOW() - INTERVAL '1 day';
5. Otimização de performance com índices e compressão
Índices são cruciais para consultas rápidas. Crie um índice composto que inclua tempo e espaço:
CREATE INDEX idx_sensor_time ON sensor_data (sensor_id, time DESC);
Para chunks antigos, ative a compressão nativa:
ALTER TABLE sensor_data SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'sensor_id',
timescaledb.compress_orderby = 'time DESC'
);
SELECT add_compression_policy('sensor_data', INTERVAL '7 days');
A compressão reduz drasticamente o armazenamento. Para retenção automática:
SELECT add_retention_policy('sensor_data', INTERVAL '90 days');
Dados com mais de 90 dias serão descartados automaticamente.
6. Integração com ferramentas do ecossistema PostgreSQL
Por ser uma extensão, TimescaleDB funciona com qualquer ferramenta que suporte PostgreSQL. ORMs como Prisma e TypeORM operam sem modificações — basta apontar a conexão. No Prisma, o schema é idêntico ao de uma tabela comum:
model SensorData {
time DateTime
sensorId Int @map("sensor_id")
temperature Float?
humidity Float?
@@map("sensor_data")
}
Para dashboards, Grafana se conecta nativamente ao TimescaleDB como fonte PostgreSQL. Use queries SQL diretas com time_bucket() para criar gráficos de séries temporais. Backup e replicação usam as mesmas ferramentas do PostgreSQL: pg_dump, pg_restore, streaming replication com pg_basebackup.
7. Casos de uso reais e boas práticas
Monitoramento de infraestrutura: colete CPU, memória e latência de rede a cada 10 segundos. Use chunks de 6 horas para consultas recentes rápidas e compressão após 2 dias. Política de retenção de 6 meses.
Análise financeira: cotações de ações com timestamp ao milissegundo. Use time_bucket('1 minute', time) para candles de 1 minuto, first() e last() para abertura e fechamento, funções de janela para médias móveis.
IoT e sensoriamento: milhares de sensores enviando leituras a cada minuto. Defina chunk_time_interval de 1 dia e particione por sensor_id na compressão. Use ingestão em lote com COPY para dados históricos e streaming com INSERT para dados em tempo real.
Boas práticas essenciais:
- Escolha chunk_time_interval baseado no volume diário: 1 dia para 10 milhões de linhas/dia, 1 semana para 1 milhão/dia.
- Sempre crie índices compostos com (sensor_id, time DESC).
- Ative compressão após 7 dias para dados históricos.
- Use time_bucket() em vez de date_trunc() para performance superior em agregações.
8. Limitações, alternativas e futuro do TimescaleDB
TimescaleDB não é bala de prata. Para workloads com cardinalidade extremamente alta (milhões de séries únicas) e ingestão pura sem consultas complexas, InfluxDB 3.0 oferece performance superior. ClickHouse é melhor para análises agregadas massivas em data lakes. TimescaleDB perde quando você precisa de consultas em tempo real com latência sub-milissegundo em bilhões de pontos — nesse caso, bancos em memória como RedisTimeseries são mais adequados.
O roadmap da comunidade inclui TimescaleDB 3.0 com suporte a multi-nó nativo (sem depender de extensões externas), compressão ainda mais agressiva e melhorias em consultas distribuídas. A versão atual (2.x) já oferece alta disponibilidade via streaming replication do PostgreSQL.
Para quem precisa de séries temporais sem abandonar o ecossistema SQL, TimescaleDB é a escolha mais equilibrada: você ganha performance especializada sem perder flexibilidade relacional.
Referências
- Documentação oficial do TimescaleDB — Guia completo de instalação, hypertables, chunks, compressão e políticas de retenção.
- TimescaleDB vs. InfluxDB: comparação detalhada — Artigo técnico comparando performance, SQL vs. Flux, e casos de uso ideais.
- Tutorial: séries temporais com Grafana e TimescaleDB — Passo a passo para criar dashboards de monitoramento com queries SQL.
- Boas práticas de modelagem de dados temporais — Documentação oficial sobre escolha de chunk interval, índices e compressão.
- TimescaleDB na prática: monitoramento de IoT — Estudo de caso real com sensores, ingestão em lote e consultas analíticas.
- Repositório GitHub do TimescaleDB — Código-fonte, issues, roadmap e contribuições da comunidade.