Architettura di dati e sistemi, dalle fondamenta Lezione 21 / 80

Database time-series: Influx, Timescale, Prometheus

Quando timestamp-più-valore è il 99% dei tuoi dati. Le ottimizzazioni che permettono ai time-series store di battere i database general-purpose di 10x o più.

Esiste una classe di workload in cui la stessa riga si ripete all’infinito, si muove solo il timestamp, e le domande che ti fai sopra hanno quasi sempre la forma “qual è la media di questa misurazione negli ultimi quindici minuti.” Application metrics. Infrastructure metrics. Letture di sensori IoT. Tick data finanziari. Se hai mai fissato una dashboard di Grafana, hai guardato dati time-series. Questa lezione parla del perché questa forma meriti la sua famiglia di database, di cosa fa quella famiglia per vincere, e di quale delle tre opzioni reali dovresti scegliere.

Il numero clamoroso, prima di entrare nei meccanismi, è che un database time-series batterà un database general-purpose su workload time-series di un fattore da dieci a cinquanta, sullo stesso hardware, sugli stessi dati. Non è una claim di marketing. È la conseguenza di tre o quattro decisioni di design che i time-series store condividono tutti, e che Postgres, MySQL e compagni non hanno. Le decisioni sono interessanti di per sé: sono un piccolo case study di cosa succede quando specializzi uno storage layer su una sola forma di dati.

La forma dei dati

Un punto time-series ha una struttura piccola e prevedibile. C’è un timestamp (di solito con precisione al nanosecondo o microsecondo). C’è un measurement name, che è la metrica che stai registrando: cpu_usage, request_count, temperature. C’è un set di tag, che sono coppie chiave-valore che identificano a cosa si riferisce questa misurazione: host=web-01, region=eu-west, service=checkout. E ci sono uno o più valori, i numeri effettivi registrati.

Concettualmente un singolo punto ha questo aspetto:

2025-12-12T10:14:32.451Z  cpu_usage{host=web-01, region=eu-west}  47.3

Ne registri milioni al minuto. La stessa combinazione (measurement, tag-set), chiamata series, riceve un nuovo punto a ogni intervallo di scrape, per sempre. Le letture hanno quasi sempre la forma “per le series che corrispondono a questo filtro, dammi i punti tra il tempo A e il tempo B, eventualmente aggregati in bucket di dimensione N.” Le scritture sono quasi sempre insert in testa alla series; update e delete di singoli punti sono rari o inesistenti.

Questo è un workload molto più stretto di quello per cui è progettato un database general-purpose. La strettezza è l’opportunità.

Perché i database general-purpose faticano

Postgres può memorizzare dati time-series. Tantissime persone fanno esattamente questo nei primi mesi di un progetto. I problemi iniziano quando i dati crescono.

Il primo problema è la dimensione degli indici. Un B-tree su (series_id, timestamp) funziona bene, ma un B-tree con un miliardo di voci occupa uno spazio serio, e le scritture mettono sotto stress i livelli alti dell’albero quando le nuove voci atterrano sul valore massimo della chiave. La write amplification è reale.

Il secondo problema è il costo di storage. Una riga in Postgres ha un overhead per riga fisso (circa 24 byte per il tuple header, più overhead per colonna, più i dati effettivi). Un punto time-series è forse 24 byte di contenuto effettivo (timestamp + valore + un riferimento alla series). L’overhead raddoppia o triplica il tuo storage. Alla scala in cui il time-series conta, la bolletta dello storage è la bolletta.

Il terzo problema è la cancellazione. I dati time-series hanno una retention naturale: tieni i dati dell’ultima settimana a piena risoluzione, quelli dell’ultimo mese in versione downsampled, quelli dell’ultimo anno ancora più grossolani. In Postgres, cancellare un miliardo di righe vecchie è un’operazione costosa, e il lavoro di autovacuum che segue è ancora più costoso. Le partition table aiutano, e fare drop di partizioni è veloce, ma devi configurarlo esplicitamente e tenergli dietro.

Il quarto problema sono i query pattern che spaziano su range enormi. “Media di cpu_usage nell’ultimo giorno, in bucket di un minuto” contro una tabella da un miliardo di righe è un sequential scan su una fetta grande. Senza indici specializzati e pre-aggregazione, è semplicemente lento.

I database time-series risolvono ciascuno di questi punti esplicitamente.

Cosa fanno i database time-series

Le ottimizzazioni variano nei dettagli tra i prodotti, ma condividono tutte all’incirca la stessa cassetta degli attrezzi.

Scritture quasi solo append, ordinate per tempo. Internamente lo storage è organizzato in modo che le scritture recenti finiscano in un buffer in memoria che viene poi flushato su file immutabili, ordinati per tempo. Non ci sono scritture random sui dati vecchi. Le insert costano quasi nulla. La struttura è simile in spirito a un LSM tree, specializzato per chiavi ordinate nel tempo.

Storage colonnare con compressione. Invece di memorizzare ogni punto come una riga, i time-series store tengono ogni series come una colonna di valori più una colonna di timestamp. La stessa metrica ripetuta riga dopo riga si comprime estremamente bene: i numeri tipici sono compressioni da 10x a 50x a seconda della metrica (un cpu_usage che oscilla intorno al 30% si comprime molto meglio di un ID intero random). La compressione è la differenza tra una bolletta di storage da multi-terabyte e una da poche centinaia di gigabyte.

Aggregazione in fase di scrittura. Man mano che i punti arrivano, il database può mantenere riassunti rolled-up: medie, somme, conteggi al minuto, all’ora, al giorno. Quando chiedi “media all’ora nell’ultima settimana,” la query legge i rollup orari, non i punti raw. Le tabelle di rollup sono minuscole rispetto ai dati raw, e il costo della query crolla di ordini di grandezza.

Retention basata su TTL. “Cancella i punti più vecchi di N giorni” è una primitiva built-in. L’implementazione di solito consiste nel droppare intere partizioni temporali, che è un’operazione di metadati piuttosto che una delete riga per riga. La retention è economica e affidabile.

Queste sono le quattro mosse. Ogni time-series store fa una qualche versione di tutte e quattro. Le differenze stanno nella superficie: il query language, il modello operativo, l’integrazione con il resto del tuo stack.

Le tre opzioni reali

Esiste un intero ecosistema di database time-series, ma nel 2026 le scelte pratiche per un nuovo progetto si restringono a tre.

Prometheus: lo standard delle metriche

Prometheus è il sistema di metriche dominante nel mondo cloud-native. È pull-based: lanci Prometheus come server, e questo fa scrape delle metriche via HTTP da una lista di target ogni quindici secondi (o all’intervallo che imposti). I target espongono un endpoint /metrics con i valori correnti; Prometheus fa pull e memorizza. Non c’è un agent client-side che pusha i dati nel database. Il modello è insolito e, una volta interiorizzato, molto piacevole: i servizi non hanno bisogno di sapere dove vanno le metriche, devono solo esporre un endpoint.

Prometheus memorizza i dati localmente, su un singolo server, nel suo formato time-series. La retention è tipicamente da quindici a trenta giorni. Il query language è PromQL, denso, potente, e vale la pena impararlo se stai facendo qualunque lavoro serio in questo spazio.

Il caveat cruciale: Prometheus non è uno store long-term. È un sistema single-server, e il pattern raccomandato per tenere anni di dati è abbinare Prometheus a uno store long-term. I nomi standard sono Thanos, Cortex, e Grafana Mimir, tutti che mettono Prometheus dietro a un object storage (S3, GCS) e forniscono scala orizzontale e retention più lunga. Scegline uno in base al modello operativo che preferisci; il data model è lo stesso in tutti e tre.

InfluxDB: il database time-series classico

InfluxDB è il prodotto originale “costruiamo un database time-series da zero,” partito nel 2013 e ancora in giro. È push-based di default, con un wire protocol verso cui i client possono scrivere direttamente. Le retention policy e il downsampling sono built-in. Il query language ha avuto una storia turbolenta: l’originale era InfluxQL (SQL-like), poi sono passati a Flux (un query language funzionale), poi hanno fatto marcia indietro su Flux e sono tornati a InfluxQL più SQL opzionale. Se stai guardando InfluxDB oggi, assicurati di capire quale versione stai guardando e quale query language è quello attuale.

La storia del prodotto è stata abbastanza accidentata che oggi non sceglierei InfluxDB per un progetto greenfield, a meno che non ci sia una ragione specifica (un investimento esistente, una feature che ha solo Influx). Le due opzioni più noiose qui sotto coprono la maggior parte delle esigenze.

TimescaleDB: time-series in Postgres

TimescaleDB è un’estensione di Postgres. La installi sul tuo Postgres server esistente, crei delle hypertable (il wrapper di Timescale sopra le partitioned table), e ottieni performance time-series con tutta la superficie di Postgres: SQL, join, indici, foreign key, il resto dell’ecosistema.

Per i team che sono già su Postgres, questa è spesso la risposta giusta. Non fai girare un secondo database. Il query language è l’SQL che già conosci. Puoi fare join dei tuoi dati time-series contro i tuoi dati relazionali in una singola query. Le feature di downsampling e retention sono di prima classe.

Il trade-off è che il throughput per nodo di Timescale è più basso di uno store specializzato come InfluxDB o di un sistema OLAP colonnare come ClickHouse. Per la maggior parte dei workload di application-metrics o business-metrics, il throughput è sufficiente. Per dati IoT o trading ad altissimo volume, potresti starci stretto.

ClickHouse: l’opzione OLAP colonnare

Cito qui ClickHouse anche se non è strettamente un database time-series, perché fa il lavoro del time-series estremamente bene. ClickHouse è un database OLAP colonnare progettato per query analitiche su grandi volumi di dati, e il time-series è uno dei workload in cui eccelle. Lo storage è colonnare, la compressione è eccellente, il query language è SQL con estensioni, e il throughput è molto alto.

Copriremo ClickHouse a dovere nel modulo 8 quando arriveremo all’OLAP. Per ora, sappi che se il tuo workload time-series ha anche query in stile analytics (group-by su molte dimensioni, aggregazioni complesse), ClickHouse vale uno sguardo.

La pipeline

La maggior parte dei setup di metriche in produzione si assomigliano, indipendentemente da quale store ci sia in mezzo. I servizi sono instrumentati (con le client library di Prometheus, OpenTelemetry, o agent vendor-specific). Prometheus fa scrape. Lo storage long-term tiene i dati storici. Grafana fa query su entrambi per le dashboard.

flowchart LR
    A[Service A: /metrics] --> P[Prometheus]
    B[Service B: /metrics] --> P
    C[Service C: /metrics] --> P
    P --> LT[Long-term store: Thanos/Mimir]
    P --> G[Grafana]
    LT --> G
    G --> U[User dashboards]

La separazione tra Prometheus (recente, veloce) e lo store long-term (storico, più economico) è il pattern standard. Le query per l’ultima ora vanno a Prometheus; le query per l’ultimo trimestre vanno a Thanos. Grafana astrae la differenza.

La trappola della cardinalità

C’è una insidia operativa che vale la pena segnalare perché ogni team che usa un database time-series ci sbatte prima o poi: la cardinality explosion.

Il numero di series distinte nel tuo database è il prodotto del numero di valori distinti per ciascun tag. Se hai una metrica con tre tag (host, region, service), e ogni tag ha dieci valori distinti, hai mille series. Gestibile. Se uno di quei tag è user_id, con un milione di valori distinti, ora hai cento milioni di series. Il database farà fatica. L’uso di memoria esplode, le performance delle query crollano, e il sistema potrebbe semplicemente rifiutarsi di accettare nuove scritture.

La regola è: i tag devono essere a bassa cardinalità. Cose come host, region, service, endpoint, status_code vanno bene. Cose come user_id, request_id, session_id, ip_address sono pericolose. Se ti ritrovi a voler mettere un identificatore ad alta cardinalità come tag, quasi certamente vuoi un sistema diverso: uno store di logging (Loki, Elasticsearch) o uno di tracing (Jaeger, Tempo), non uno di metriche.

Ogni team lo impara una volta, nel modo doloroso. Adesso l’hai imparato nel modo facile.

Casi d’uso che calzano

I workload in cui i database time-series sono inequivocabilmente la risposta giusta:

  • Application metrics: request rate, latency, error rate, saturation. I quattro classici “golden signal.” Sono questi che alimentano ogni dashboard di produzione.
  • Infrastructure metrics: CPU, memoria, disco, rete. Il substrato su cui gira tutto il resto.
  • Dati di sensori IoT: letture di temperatura, posizioni GPS, telemetria dei dispositivi. Spesso partizionati per device ID, il che significa che devi stare attento alla cardinalità (un milione di dispositivi va bene; un miliardo no, senza un design speciale).
  • Tick data finanziari: prezzi per strumento, campionati ad alta frequenza. Qui dominano ClickHouse e KDB+, ma anche TimescaleDB compare in questo spazio.

Mettendo tutto insieme

La raccomandazione, per la maggior parte dei team nel 2026, è breve. Per le metriche in uno stack cloud-native, usa Prometheus, abbinato a Thanos o Mimir per lo storage long-term, interrogato attraverso Grafana. Per workload time-series embedded in un’applicazione che usa già Postgres, usa TimescaleDB ed evita di far girare un secondo database. Per time-series con tanta analytics e altissimo volume, guarda ClickHouse. Se ti ritrovi a tendere la mano verso InfluxDB, assicurati di avere una ragione specifica.

La proprietà condivisa di tutti questi è che sono specializzati. Battono Postgres su workload time-series con ampio margine, e perdono contro Postgres su qualunque cosa non sia un workload time-series. Usa lo strumento giusto. Il costo di far girare uno store specializzato accanto al tuo database principale è quasi sempre minore del costo di provare a fare tutto in uno solo.

Citazioni e letture aggiuntive

  • Prometheus documentation, https://prometheus.io/docs/ (retrieved 2026-05-01). The reference for PromQL, the scrape model, and the long-term storage options.
  • TimescaleDB documentation, https://docs.timescale.com/ (retrieved 2026-05-01). Hypertables, continuous aggregates, retention policies.
  • InfluxDB documentation, https://docs.influxdata.com/ (retrieved 2026-05-01). The InfluxDB 3 line and the recent return to SQL.
  • Brian Brazil, “Prometheus: Up and Running” (O’Reilly, 2018). Still the best book for getting deep into PromQL and the operational model. A second edition is in progress.
  • Grafana Labs, “Grafana Mimir documentation”, https://grafana.com/docs/mimir/latest/ (retrieved 2026-05-01). The long-term-store option that has been gaining the most ground in recent years.
Cerca