This page covers the conceptual storage model. For exact byte-level encoding
schemas, see the
storage RFC on GitHub.
Storage components
Timeseries has three storage components that work together to serve queries:| Component | Purpose |
|---|---|
| Raw series storage | Stores (timestamp, value) pairs keyed by series ID |
| Inverted index | Maps label/value pairs to series IDs for efficient label matching |
| Forward index | Maps series IDs back to their full label sets |
How a query uses the storage components
To illustrate how these components interact, consider the querysum by (instance) (metric{path="/query", method="GET", status="200"}):
- Parse the query into a query plan.
- Look up each label selector in the inverted index
(
__name__="metric",path="/query",method="GET",status="200") to find matching series IDs. Intersect the results. - Resolve each matched series ID to its full label set in the forward index
(including
instance, needed for theby (instance)grouping). - Read the
(timestamp, value)pairs for each matched series in the raw series storage matched series, aggregate, and return the result.
Time buckets
Data is divided into time buckets, where each bucket holds all data received within a specific window of time. The bucket boundary is encoded directly into every record key, which means SlateDB physically clusters records from the same time window together on disk. This design provides two key benefits:- Efficient range scans: queries that target recent data only scan the relevant buckets without reading historical data.
- Scoped cardinality: series IDs are local to a time bucket, so cardinality is bounded by the number of series in each bucket rather than growing unboundedly over time.
Record types
All records share a common key prefix that encodes a version byte and a record tag. The record tag identifies the record type and, for bucket-scoped records, the time granularity. This allows different record types and bucket sizes to coexist in the same keyspace while maintaining clean sort ordering.| Record type | Scope | Description |
|---|---|---|
| BucketList | Global | Enumerates the time buckets that contain data, including each bucket’s granularity |
| SeriesDictionary | Bucket | Maps label-set fingerprints to series IDs, used during ingestion to assign or look up IDs |
| ForwardIndex | Bucket | Stores the full label set for each series ID, used during query execution to resolve labels |
| InvertedIndex | Bucket | Maps each label/value pair to a RoaringBitmap of series IDs (the posting list) |
| TimeSeries | Bucket | Holds the Gorilla-compressed (timestamp, value) stream for each series |
Inverted index
The inverted index is the primary mechanism for label-based queries. For every label/value pair on a series, the index stores a posting list of all series IDs that share that pair. For example, if series 713 has labelsjob="api" and
status="500", then both posting lists include series 713.
Posting lists are stored as RoaringBitmaps,
which provide efficient compression and fast set operations (intersection,
union) for combining multiple label selectors.
The metric name (__name__) is treated as a regular label rather than a
first-class key prefix. This gives the query planner flexibility to choose the
most selective label to scan first. For example, sometimes filtering by
cluster="prod" is more efficient than filtering by metric name.
Forward index
The forward index maps each series ID back to its canonical label set. After the inverted index identifies matching series, the forward index resolves each one to its full set of labels. This is necessary for operations likeby and
without grouping, which need labels that weren’t part of the original
selector.
The forward index also stores metric metadata: the metric type
(gauge, sum, histogram, exponential histogram, summary), temporality, and
the monotonic flag.
Time series storage and compression
Raw time series data is stored as Gorilla-compressed streams of(timestamp, value) pairs. Gorilla
compression exploits the
temporal locality of time series data to achieve high compression ratios. This
works by encoding timestamps using delta-of-delta encoding and values using XOR
compression.
Metric type handling
Following the Prometheus approach, all OpenTelemetry metric types are normalized tof64 values:
-
Gauges and counters are stored directly as
(timestamp, value)pairs. -
Histograms are decomposed into multiple series using Prometheus naming
conventions:
metric_bucket{le="<upper>"}for each bucket boundary (cumulative counts, with a finalle="+Inf"bucket)metric_sumfor the sum of all observationsmetric_countfor the total number of observations