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