Timeseries Engine
Timeseries is a peer engine sharing storage with columnar and spatial. It adds retention policies, continuous aggregation, ILP ingest, and dedicated time-series SQL functions. Data is stored in the same compressed-column memtables with a TIME_KEY column driving partition-by-time and block-level skip.
Selected via WITH (engine='timeseries') on CREATE COLLECTION, or the CREATE TIMESERIES <name> convenience alias.
DDL
CREATE COLLECTION cpu_metrics (
ts TIMESTAMP TIME_KEY,
host VARCHAR,
region VARCHAR,
cpu_usage FLOAT,
mem_usage FLOAT
) WITH (engine='timeseries', partition_by='1d', retention='90d');
-- Convenience alias
CREATE TIMESERIES cpu_metrics;
Partitions
Rows land in an in-memory buffer first. A flush of that buffer writes one sealed partition for each partition_by interval its rows fall in. The partitions of one flush appear together: a crash leaves all of them or none.
partition_by | Interval |
| not set | One hour. A flush whose rows span several hours writes one partition per hour. |
30s, 15m, 1h, 1d, 1w | A fixed duration, aligned to multiples of itself from the Unix epoch. |
1M | A calendar month, starting at 00:00 UTC on the first day. |
1y | A calendar year, starting at 00:00 UTC on 1 January. |
UNBOUNDED | One partition per flush. |
AUTO | Starts at one day. It narrows to one hour once a core ingests more than 100,000 rows a second. |
ALTER TIMESERIES <name> SET (partition_by = '…')applies to later flushes only. A partition already on disk keeps the interval it was cut at.- A late row, one whose interval an earlier flush already sealed, forms a new partition in that interval. A sealed partition is never rewritten to take it. The maintenance tick later merges the partitions of that interval (see Merging).
-- List the partitions of a timeseries collection
SHOW PARTITIONS FOR cpu_metrics;
-- Rewrite every sealed partition with the current schema and codecs
REWRITE PARTITIONS FOR cpu_metrics;
SHOW PARTITIONS FOR returns one row per partition:
| Column | Meaning |
partition | The partition name. |
state | Active, Sealed or Merged. |
min_ts | The earliest row timestamp, in milliseconds. |
max_ts | The latest row timestamp, in milliseconds. |
rows | The row count. |
size | The size on disk. |
interval_ms | The width of the interval the partition was cut at. 0 means UNBOUNDED. |
REWRITE PARTITIONS FOR rewrites each sealed partition and drops every column the collection no longer has. The rows do not change. The statement returns after the last partition is rewritten and reports how many partitions it rewrote. It needs the ALTER privilege on the collection.
Both statements read the collection in the current database. Neither runs inside a transaction. SHOW PARTITIONS ON <stream> is a different statement: it lists a change stream's partitions (see Consumer Groups).
Merging
Late rows leave an interval with several partitions that overlap in time. The maintenance tick merges the partitions of one interval into one Merged partition. A merge moves rows and never changes them.
- A merge runs on the core that owns the collection, and on each replica for its own partitions. Partition names differ between replicas. Rows do not.
- An interval merges once it ended
merge_quiet_msago, so late rows still arriving for it flush first.COMPACT <name>runs every merge at once, including the merge of an interval that has not ended. - A merged partition stays within one interval, so retention removes its rows at most one interval later than it would remove the separate partitions. On a bitemporal collection, partitions whose system times lie more than one interval apart do not merge.
- A
UNBOUNDEDcollection has no interval to merge within. Its partitions do not merge. - A crash at any point leaves every row in exactly one partition. Restart finishes a merge that committed and discards one that did not.
- Encrypted partitions merge with the segment key, and the merged partition is encrypted with it.
A merge keeps every continuous aggregate exact. It waits while an aggregate has folded some partitions of the interval and not others:
- An
on_flushoron_sealaggregate folds the missing partitions on the next tick, and the interval merges after that. - An interval-refreshed or
manualaggregate folds them at its next refresh. Amanualaggregate that is never refreshed keeps such an interval unmerged.
[tuning.timeseries] key | Default | Meaning |
merge_groups_per_tick | 2 | Partition groups one maintenance tick merges. 0 turns merging off. COMPACT ignores it. |
merge_max_rows | 4000000 | Most rows one merge reads and writes. A larger interval merges in several groups. |
merge_quiet_ms | 60000 | How long after an interval ends its partitions merge. |
Queries
-- Time-bucketed aggregation
SELECT time_bucket('5 minutes', ts) AS bucket, host, AVG(cpu_usage) AS avg_cpu
FROM cpu_metrics
WHERE ts > now() - INTERVAL '1 hour'
GROUP BY bucket, host ORDER BY bucket DESC;
-- Approximate aggregation (mergeable across shards)
SELECT approx_count_distinct(host), approx_percentile(cpu_usage, 0.95)
FROM cpu_metrics WHERE ts > now() - INTERVAL '24 hours';
Continuous Aggregation
A continuous aggregate folds new rows into per-bucket state. It never re-scans the source.
CREATE CONTINUOUS AGGREGATE cpu_hourly ON cpu_metrics
BUCKET '1h'
AGGREGATE avg(cpu_usage) AS avg_cpu, ts_percentile(cpu_usage, 0.99) AS p99_cpu
GROUP BY host
WITH (refresh_interval = '1m', retention = '365d');
REFRESH CONTINUOUS AGGREGATE cpu_hourly;
BUCKET sets the bucket width. Do not use time_bucket(...) in the definition.
| Clause | Meaning |
AGGREGATE f(col) [AS alias], ... | Outputs. Functions: count, sum, avg, min, max, first, last, count_distinct, approx_count_distinct, percentile(col, q) (also ts_percentile, approx_percentile), topk(col, k) (also approx_topk). q is in [0, 1]. k is at least 1. Use count(*) to count rows. |
GROUP BY col, ... | Optional. Adds group columns to each bucket. |
refresh_policy | on_flush (default), on_seal, manual, or an interval such as '5m' (folds periodically). |
refresh_interval | Interval such as '1m'. Folds periodically on the maintenance tick. |
retention | Interval after which the aggregate drops old buckets. |
Refresh behavior:
on_flushandon_sealfold each flushed partition once.- An interval
refresh_policyorrefresh_intervalfolds on the maintenance tick at that interval. manualfolds only onREFRESH CONTINUOUS AGGREGATE. A read serves the last refresh.
Aggregate state survives restart. It keeps its history after raw retention removes the source rows.
SELECT * FROM cpu_hourly returns one row per bucket and group. Columns are bucket, the GROUP BY columns, then the outputs. The aggregate is read-only.
Timeseries SQL Functions
| Function | What it does |
ts_rate | Per-second rate of change |
ts_delta | Difference between consecutive values |
ts_moving_avg | Moving average over a window |
ts_ema | Exponential moving average |
ts_interpolate | Gap-fill with interpolated values |
ts_percentile | Percentile calculation |
ts_zscore | Z-score anomaly detection |
ts_bollinger_upper/lower/mid/width | Bollinger Bands |
ts_moving_percentile | Rolling percentile |
ts_correlate | Correlation between two series |
ts_lag / ts_lead | Previous/next value in a series |
ILP Ingest
Enable with ports.ilp = 8086. Any ILP-compatible client (Telegraf, Vector) pushes metrics directly:
echo "cpu,host=web-01 usage=72.5 1609459200000000000" | nc localhost 8086
Adaptive batching and per-series core routing — self-tuning, no configuration needed.
Grafana / PromQL
NodeDB works as a native Grafana Prometheus data source at http://nodedb:6480/v1/obsv/api. Full PromQL engine (Tier 1+2+3 functions). Also supports Prometheus remote write/read for long-term storage.