Skip to main content

timescaledb-hyperfunctions

Use this skill when writing analytical SQL over time-series data with the TimescaleDB Toolkit (timescaledb_toolkit extension) hyperfunctions: approximate percentiles, statistical summaries, time-weighted averages, counter/gauge rates, uptime/heartbeat tracking, state durations, OHLC candlesticks, approximate distinct counts, top-N, and downsampling. **Trigger when user asks to:** - Compute percentiles/medians/p95/p99 over large or rolled-up time-series data - Compute rates or deltas from monotonic counters (Prometheus-style) or gauges - Compute time-weighted averages or integrals over irregularly sampled data - Track uptime/downtime from heartbeats, or time spent in each state - Build OHLC/candlestick or VWAP data for financial ticks - Store re-aggregatable summaries in continuous aggregates (two-step aggregation, rollup) - Approximate COUNT DISTINCT, find top-N / most frequent values, or downsample for charts **Keywords:** timescaledb_toolkit, hyperfunctions, percentile_agg, uddsketch, tdigest, approx_perc

Source facts

Repository
timescale/pg-aiguide
Last source activity
October 7, 2026 at 20:18
Detected SKILL.md language
English
Stars
1,869
Forks
117

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
timescaledb-hyperfunctions
description
Use this skill when writing analytical SQL over time-series data with the TimescaleDB Toolkit (timescaledb_toolkit extension) hyperfunctions: approximate percentiles, statistical summaries, time-weighted averages, counter/gauge rates, uptime/heartbeat tracking, state durations, OHLC candlesticks, approximate distinct counts, top-N, and downsampling. **Trigger when user asks to:** - Compute percentiles/medians/p95/p99 over large or rolled-up time-series data - Compute rates or deltas from monotonic counters (Prometheus-style) or gauges - Compute time-weighted averages or integrals over irregularly sampled data - Track uptime/downtime from heartbeats, or time spent in each state - Build OHLC/candlestick or VWAP data for financial ticks - Store re-aggregatable summaries in continuous aggregates (two-step aggregation, rollup) - Approximate COUNT DISTINCT, find top-N / most frequent values, or downsample for charts **Keywords:** timescaledb_toolkit, hyperfunctions, percentile_agg, uddsketch, tdigest, approx_percentile, stats_agg, time_weight, counter_agg, gauge_agg, heartbeat_agg, state_agg, candlestick_agg, hyperloglog, approx_count_distinct, min_n, max_n, mcv_agg, lttb, asap_smooth, rollup, two-step aggregation
license
Apache-2.0
compatibility
Requires PostgreSQL 15+ with TimescaleDB and the timescaledb_toolkit extension (1.16+; gauge_agg needs 1.25+)
metadata
{"author":"tigerdata"}
# TimescaleDB Toolkit Hyperfunctions Hyperfunctions are SQL aggregates and accessors from the `timescaledb_toolkit` extension for analysis that plain PostgreSQL aggregates handle badly: percentiles that can be re-aggregated, rates over resetting counters, averages over irregular samples, uptime, state durations, and more. ## Setup ```sql CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit; -- preinstalled on Tiger Cloud SELECT extversion FROM pg_extension WHERE extname = 'timescaledb_toolkit'; ALTER EXTENSION timescaledb_toolkit UPDATE; -- after upgrading the package ``` **Only use functions from the default schema in persistent objects.** Anything in the `toolkit_experimental` schema can change between releases, and `ALTER EXTENSION ... UPDATE` **drops** views, continuous aggregates, and functions that depend on it. All functions in this skill are stable. ## The Two-Step Pattern (read this first) Nearly every hyperfunction works in two steps: 1. An **aggregate** builds a compact summary (`percentile_agg(val)` → `uddsketch`, `time_weight(...)` → `timeweightsummary`, ...). 2. An **accessor** extracts a result from the summary (`approx_percentile(0.95, summary)`, `average(summary)`, ...). ```sql -- Function-call style SELECT approx_percentile(0.95, percentile_agg(latency_ms)) FROM requests; -- Arrow style (same result, reads left to right, chains multiple accessors) SELECT percentile_agg(latency_ms) -> approx_percentile(0.95) FROM requests; ``` The pattern exists so summaries can be **stored and re-aggregated**: - **`rollup(summary)`** merges summaries correctly. Use it to go from hourly to daily buckets. - **Combining entities depends on the summary type.** Distribution summaries (`percentile_agg`, `uddsketch`, `tdigest`, `stats_agg`, `hyperloglog`, `mcv_agg`, `min_n`/`max_n`) can be rolled up across entities, e.g. one p99 for all hosts. Time-series summaries (`time_weight`, `counter_agg`, `gauge_agg`, `heartbeat_agg`, `state_agg`) describe one series. They can only be rolled up over consecutive, non-overlapping time ranges of the **same** entity: rolling up overlapping summaries from different hosts raises an ordering error. Compute the per-entity result first, then combine the numbers (e.g. `SUM` of per-host rates for a fleet-wide rate). - **Never average averages, percentiles, or rates.** `AVG(p95_hourly)` is not a daily p95. `rollup(hourly_sketch) -> approx_percentile(0.95)` is the correct value. - Store the **summary** in a continuous aggregate, not the final number. Then any accessor works at query time. ## Choosing a Function | Question | Aggregate | Key accessors | |---|---|---| | p50/p95/p99, median | `percentile_agg`, `uddsketch`, `tdigest` | `approx_percentile`, `approx_percentile_array`, `approx_percentile_rank`, `mean`, `error` | | avg/stddev/variance that can be rolled up; regression | `stats_agg` (1D or 2D) | `average`, `stddev`, `variance`, `skewness`, `kurtosis`, `num_vals`, `sum`; 2D: `slope`, `intercept`, `corr`, `determination_coeff`, `covariance` | | Average over irregular sampling | `time_weight` | `average`, `integral`, `interpolated_average`, `first_val`, `last_val` | | Rate/delta of monotonic counters with resets | `counter_agg` | `delta`, `rate`, `extrapolated_rate`, `irate_right`, `num_resets`, `interpolated_rate` | | Rate/delta/trend of gauges (no reset handling) | `gauge_agg` (1.25+) | `delta`, `rate`, `extrapolated_rate`, `irate_right`, `slope`, `num_changes`, `interpolated_delta` | | Uptime/downtime from heartbeats | `heartbeat_agg` | `uptime`, `downtime`, `live_ranges`, `dead_ranges`, `live_at`, `num_gaps`, `interpolated_uptime` | | Time spent in each state | `state_agg` | `duration_in`, `state_timeline`, `state_periods`, `state_at`, `into_values` | | OHLC / VWAP | `candlestick_agg` (ticks), `candlestick` (pre-aggregated bars) | `open`, `high`, `low`, `close`, `volume`, `vwap`, `open_time`, ... | | Approx COUNT DISTINCT | `hyperloglog`, `approx_count_distinct` | `distinct_count`, `stderror` | | Top-N / bottom-N values (with rows) | `max_n`, `min_n`, `max_n_by`, `min_n_by` | `into_values`, `into_array` | | Most frequent values | `mcv_agg` | `topn`, `max_frequency`, `min_frequency`, `into_values` | | Downsample for charting | `lttb`, `asap_smooth` | `unnest` | TimescaleDB core (not the toolkit) provides `time_bucket`, `time_bucket_gapfill`, `locf`, `interpolate`, `first`, and `last`. Combine them freely with hyperfunctions. ## Percentiles ```sql SELECT time_bucket('5 minutes', ts) AS bucket, service, percentile_agg(latency_ms) -> approx_percentile(0.5) AS p50, percentile_agg(latency_ms) -> approx_percentile(0.99) AS p99 FROM requests WHERE ts > now() - INTERVAL '1 day' GROUP BY bucket, service; -- Several percentiles from one sketch; fraction of requests under 200 ms WITH s AS (SELECT percentile_agg(latency_ms) AS sketch FROM requests) SELECT approx_percentile_array(ARRAY[0.5, 0.9, 0.99], sketch), approx_percentile_rank(200, sketch) FROM s; ``` - **`percentile_agg(value)`**: default choice. It is a `uddsketch` with sensible defaults and a bounded relative error. - **`uddsketch(size, max_error, value)`**, e.g. `uddsketch(200, 0.001, v)`: guaranteed relative error. Use when you need to tune accuracy or memory. - **`tdigest(buckets, value)`**, e.g. `tdigest(100, v)`: more accurate at extreme tails (p99.9). Error is not bounded, and results depend on input order. - Results are approximate. Use `PERCENTILE_CONT` when exact values are required and the data is small. ## Statistical Summaries ```sql -- Re-aggregatable average/stddev SELECT device_id, stats_agg(temperature) -> average() AS avg_temp, stats_agg(temperature) -> stddev() AS sd_temp FROM readings GROUP BY device_id; -- 2D: linear regression of y on x SELECT stats_agg(power_w, temperature) -> slope() AS watts_per_degree, stats_agg(power_w, temperature) -> corr() AS correlation FROM readings; -- Rolling 7-day average from daily summaries (window function over summaries) SELECT bucket, rolling(daily_stats) OVER (ORDER BY bucket ROWS 6 PRECEDING) -> average() AS avg_7d FROM daily_rollup; ``` Accessors such as `stddev` and `variance` take an optional `'population'` or `'sample'` argument (default `'sample'`). In 2D, `stats_agg(y, x)` puts the dependent variable first. Use `average_x()`, `average_y()`, and similar accessors for per-axis stats. ## Time-Weighted Averages `AVG()` overweights periods with dense sampling. `time_weight` weights each value by how long it was in effect. ```sql SELECT time_bucket('1 hour', ts) AS bucket, sensor_id, time_weight('Linear', ts, value) -> average() AS tw_avg, time_weight('LOCF', ts, value) -> integral('hour') AS value_hours FROM readings GROUP BY bucket, sensor_id; ``` - **`'Linear'`** interpolates between points. Use it for continuously varying signals such as temperature. - **`'LOCF'`** (last observation carried forward) holds each value until the next one. Use it for set-points, prices, and state-like values. - `average()` returns NULL for a bucket with a single point, because there is no time span to weight. Use `interpolated_average` (below), or a larger bucket, for sparse series. - `integral(unit)` takes `'microsecond'`, `'millisecond'`, `'second'` (default), `'minute'`, or `'hour'`. **Bucket edges.** A plain `average()` only sees points inside the bucket. To account for values that span bucket boundaries, pass the neighbouring buckets' summaries: ```sql WITH t AS ( SELECT time_bucket('1 hour', ts) AS bucket, sensor_id, time_weight('LOCF', ts, value) AS tw FROM readings GROUP BY 1, 2 ) SELECT bucket, sensor_id, tw -> interpolated_average( bucket, INTERVAL '1 hour', LAG(tw) OVER w, LEAD(tw) OVER w) AS tw_avg FROM t WINDOW w AS (PARTITION BY sensor_id ORDER BY bucket); ``` ## Counters and Gauges **Counters** only increase and reset to zero on restart (Prometheus counters, byte totals, request totals). `counter_agg` detects resets and corrects for them. ```sql SELECT time_bucket('5 minutes', ts) AS bucket, host, counter_agg(ts, bytes_total) -> delta() AS bytes, counter_agg(ts, bytes_total) -> rate() AS bytes_per_sec, counter_agg(ts, bytes_total) -> num_resets() AS restarts, counter_agg(ts, bytes_total) -> irate_right() AS instant_rate FROM metrics GROUP BY bucket, host; ``` - `rate()` is per second, measured between the first and last sample in the bucket. - For Prometheus-compatible extrapolation to the bucket edges, give the aggregate bounds: `counter_agg(ts, v, tstzrange(bucket, bucket + '5 min')) -> extrapolated_rate('prometheus')`. You can also set bounds afterwards with `with_bounds(summary, range)`. - For edge-accurate deltas across buckets, use `interpolated_delta(bucket, interval, LAG(summary) OVER w, LEAD(summary) OVER w)` or `interpolated_rate(...)`, following the `interpolated_average` pattern above. ### Gauges **Gauges** go up and down: memory in use, queue depth, temperature, connection count. `gauge_agg(ts, value)` gives counter-style change metrics but treats every drop as a real decrease, not a reset. Requires toolkit 1.25+. ```sql SELECT time_bucket('15 minutes', ts) AS bucket, queue, gauge_agg(ts, depth) -> delta() AS net_change, -- last - first gauge_agg(ts, depth) -> rate() AS change_per_sec, -- delta / time_delta gauge_agg(ts, depth) -> slope() AS trend_per_sec, -- least-squares fit gauge_agg(ts, depth) -> num_changes() AS changes, gauge_agg(ts, depth) -> irate_right() AS latest_rate -- last two points FROM queue_depth GROUP BY bucket, queue; ``` - **Accessors** (stable in 1.25): `delta`, `rate`, `time_delta`, `idelta_left`/`idelta_right`, `irate_left`/`irate_right`, `slope`, `intercept`, `corr`, `num_changes`, `num_elements`, `zero_time` (as `-> zero_time()`; the function form is `gauge_zero_time(summary)`), and `extrapolated_delta`/`extrapolated_rate`. The extrapolated accessors take no method argument and need bounds, from `gauge_agg(ts, v, tstzrange(...))` or `with_bounds(summary, range)`. - **Edge-accurate deltas across buckets**: call `interpolated_delta` or `interpolated_rate` in function form, with the summary first. The arrow form is not stable for gauges. ```sql SELECT bucket, queue, interpolated_delta(ga, bucket, INTERVAL '15 minutes', LAG(ga) OVER w, LEAD(ga) OVER w) AS net_change FROM (SELECT time_bucket('15 minutes', ts) AS bucket, queue, gauge_agg(ts, depth) AS ga FROM queue_depth GROUP BY 1, 2) t WINDOW w AS (PARTITION BY queue ORDER BY bucket); ``` - **Not available on gauges**: `num_resets`, and `first_val`/`last_val`. Use `time_weight` if you need first/last values or a time-weighted average of the gauge. - **Choosing gauge or counter**: if a value only ever falls on a restart, use `counter_agg`. If falls are real, such as memory being freed, use `gauge_agg`. `stats_agg` or `time_weight` are often better for "average level". `gauge_agg` answers "how much and how fast did it change". ## Heartbeats and Uptime `heartbeat_agg(ts, start, range, heartbeat_interval)`: each heartbeat at `ts` marks the system live until `ts + heartbeat_interval`. `start` and `range` define the window that the aggregate covers. ```sql -- Store per day, e.g. as continuous aggregate daily_heartbeats SELECT time_bucket('1 day', ts) AS day, service, heartbeat_agg(ts, time_bucket('1 day', ts), '1 day', '2 minutes') AS hb FROM pings GROUP BY day, service; SELECT day, service, hb -> uptime() AS uptime, hb -> downtime() AS downtime, hb -> num_gaps() AS outages, hb -> dead_ranges() AS outage_ranges -- setof (start, end) FROM daily_heartbeats; ``` - Make `start` and `range` match the bucket. Otherwise time outside the data is counted as downtime. - Use `interpolated_uptime(hb, LAG(hb) OVER w)` so a heartbeat just before the bucket boundary counts toward the next bucket. - `live_at(hb, timestamp)` checks liveness at one point in time. `rollup(hb)` merges adjacent windows. ## State Tracking `state_agg(ts, state)` takes a `text` or `bigint` state and records how long each state lasted. ```sql -- One summary per machine per day; store it, e.g. as continuous aggregate machine_states_daily SELECT time_bucket('1 day', ts) AS day, machine_id, state_agg(ts, status) AS sa FROM machine_status GROUP BY day, machine_id; SELECT day, machine_id, sa -> duration_in('running') AS running_time,
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub