Topic 30 — Time-Series Engines
Metrics workloads are the most regular data any database sees — and TSDBs are what you get when every design decision exploits that regularity: append-mostly, time-ordered, compress-by-predicting, partition-by-time, delete-by-dropping-partitions.
The problem, measured (bench lane 1, provided — runs today)
cargo run --release --bin tsdb_bench — 1 M samples per series shape, 10-second
scrape interval, raw representation = 16.00 B/sample (8 B timestamp + 8 B value):
shape delta+varint decode
constant 11.00 B/sample 272 Msamples/s
gauge 11.00 B/sample 239 Msamples/s
counter 11.00 B/sample 320 Msamples/s
random 11.00 B/sample 329 Msamples/s
11.00 bytes per sample for all four shapes, including the constant one. A series that never changes value compresses exactly as well as uniform random noise, which is absurd on its face and is precisely the finding: delta + varint encodes the timestamp well (regular scrapes delta to a small constant) and the float not at all, because an f64 that repeats is still 8 bytes of mantissa to a varint that only knows about leading zero bytes.
So the baseline has cut 16 B to 11 B — a 1.45× win, entirely from the timestamp
column — and left the larger half of every sample untouched. That is the gap
Gorilla’s XOR-and-leading-zeros trick is aimed at, and it is why the paper
reports single-digit bits per value rather than bytes. Predict the B/sample
Gorilla achieves for each of these four shapes before you implement it; the
spread between constant and random should go from zero to enormous, and if it
does not, your encoder is not exploiting what the shape gives it.
0. The shape of the problem
write path read path
~1M samples/s, tiny (ts, f64) points "cpu > 90 for job=api over 6h"
99.9% arrive in time order always a TIME RANGE
per-series arrival ~10s apart always a LABEL SELECTOR
usually an AGGREGATION
│ │
▼ ▼
amortize: batch into per-series index labels, not values:
compressed chunks (Gorilla), inverted index (name,value) -> series,
flush to immutable time-blocks then min/max-prune time blocks
Baseline measured here (see notes.md): the obvious codec — delta+varint timestamps, raw f64 values — lands at 11.00 B/sample regardless of value shape, because the 8-byte values dominate. The entire point of Gorilla’s XOR trick is attacking those 8 bytes.
1. The design space
graph TD
G["Gorilla (VLDB '15)<br/>in-memory cache: dod timestamps<br/>+ XOR floats = 1.37 B/sample"]
P["Prometheus TSDB<br/>Gorilla chunks + head/WAL +<br/>2h immutable blocks +<br/>MemPostings label index"]
VM["VictoriaMetrics<br/>same shapes, LSM-ier:<br/>nearest-delta2 + partitions +<br/>tagFilters cache"]
IOX["InfluxDB 3 / IOx<br/>TSDB rebuilt on topic 28:<br/>WAL -> Arrow buffer -> Parquet<br/>on object storage, DataFusion SQL"]
M["Monarch (VLDB '20)<br/>planet-scale: in-memory,<br/>push-based, query pushdown"]
B["BtrDB (FAST '16)<br/>nanosecond telemetry: tree of<br/>time-partitioned AGGREGATES,<br/>queries hit precomputed summaries"]
G -->|"chunk format"| P
G -->|"chunk format"| VM
P -->|"columnar + object store"| IOX
G -->|"scale out"| M
G -.->|"different regime:<br/>120 Hz/stream, 1.4M points/s/server"| B
2. The one-table summary
| system | value codec | time organization | label index | out-of-order |
|---|---|---|---|---|
| Gorilla | XOR floats, dod ts | 2h in-memory blocks | (delegated to HBase layer) | rejected |
| Prometheus | same, xor.go | head + 2h blocks, exponential compaction | MemPostings inverted index | bounded OOO window, separate buffer |
| VictoriaMetrics | nearest-delta2 + optional lossy precisionBits | monthly partitions of LSM parts | index_db + tagFilters cache | buffered in raw-rows shards |
| InfluxDB 3 | Parquet encodings | WAL → buffer → Parquet files by time | catalog + last/distinct caches | absorbed by sort at persist |
| BtrDB | delta tree | time-partitioned octree-ish tree | (few, fat streams) | version-annotated inserts |
3. Experiments (experiments/)
cargo run --release --bin tsdb_bench
| file | what | status |
|---|---|---|
gen.rs | scrape timestamps + gauge/counter/constant/random shapes, OOO arrivals, label sets | PROVIDED |
bits.rs | MSB-first BitWriter/BitReader + sign_extend | PROVIDED |
baseline.rs | zigzag varint delta codec — the thing to beat, measured at 11.00 B/sample | PROVIDED |
gorilla.rs | dod timestamp buckets + XOR-float codec | STUB |
head.rs | in-order fast path + bounded OOO window + LWW merge flush | STUB |
index.rs | MemPostings-style (name,value)→series inverted index + k-way intersect | STUB |
bin/tsdb_bench.rs | baselines (provided) + gorilla ratios + OOO tax sweep + selector latency | lanes |
Contract highlights: gorilla roundtrip must be bit-exact for every f64; constant series ≤ ~2 bits/sample while full-entropy values must fail to compress (>8 B/sample — the codec wins on regularity, not magic); head rejects samples older than the OOO window and flush output is sorted so it feeds the in-order-only encoder; index intersection matches brute force and the unique-per-series label demonstrably explodes the postings map (the cardinality bomb, counted).
4. Reading guides
- reading-gorilla.md — Gorilla: compress by predicting
- reading-prometheus-tsdb.md — Prometheus TSDB: an LSM with time as the key
- reading-victoriametrics-influx.md — VictoriaMetrics & InfluxDB 3: two rebuttals to Prometheus
- reading-monarch-btrdb.md — Monarch & BtrDB: the extremes that bracket the middle
5. Cross-topic threads
- Topic 4 (LSM): a TSDB is an LSM where the key is time — head=memtable, blocks=SSTs, compaction merges by time range, retention = drop the oldest level. VictoriaMetrics says “parts” and means it literally.
- Topic 12 (columnar): IOx is the thesis that a TSDB is just a columnar store with a time-partitioned catalog — Parquet + DataFusion replace the custom chunk format.
- Topic 23 (FTS): MemPostings IS an inverted index; label selectors are boolean term queries; high cardinality = unbounded vocabulary.
- Topic 28 (cloud-native): InfluxDB 3’s WAL→object-store pipeline is topic 28’s landing-zone lesson applied to metrics.
- Topic 29: the OOO window is a watermark — the same bounded-disorder-then-seal move as streaming watermarks (topic 27).
6. Capstone M30 (FalkorDB)
Temporal graph: edge/property history as (entity, attribute, ts) series;
MATCH ... AT TIME t = snapshot read at t over history chunks. Design
notes in notes.md §M-log.