Skip to content

OQL Functions

OQL's SELECT clause supports a fixed set of aggregate functions. An aggregate collapses many rows into one value — per group when you use BY, per time bucket when you use EVERY, or over the whole window otherwise.

SELECT avg(cpu_user_pct) FROM host LAST 5m

Aggregate functions

Function What it returns
avg(col) Arithmetic mean of col
sum(col) Sum of col
min(col) Smallest value
max(col) Largest value
count(*) Number of rows
count(col) Number of rows where col is non-null
any(col) One arbitrary value of col from the group
p50(col) 50th percentile (median)
p90(col) 90th percentile
p95(col) 95th percentile
p99(col) 99th percentile

A few notes:

  • count(*) vs count(col)count(*) counts every row; count(col) counts only rows where col has a value. Use count(*) for "how many log lines / spans / events".
  • any(col) is handy when a column is constant within a group and you just want to carry it through alongside other aggregates.
-- Row counts
SELECT count(*) FROM logs WHERE level = 'ERROR' LAST 1h

-- Min / max / avg together
SELECT min(cpu_user_pct), avg(cpu_user_pct), max(cpu_user_pct)
FROM host BY host LAST 1h

Percentiles

p50, p90, p95, and p99 answer "what value is the Nth-percentile sample?" — the standard way to talk about latency, because an average hides the slow tail.

-- p50 / p95 / p99 request latency over time
SELECT p50(duration_ns), p95(duration_ns), p99(duration_ns)
FROM spans
WHERE service = 'checkout'
EVERY 1m LAST 1h

Read it as: "99% of requests in each minute completed faster than the p99 line." Percentiles are computed per group and per bucket, exactly like the other aggregates.

Counters vs gauges

Metrics come in two flavours, and it matters which aggregate you pick.

Gauges are point-in-time readings that go up and down — CPU percentage, memory in use, queue depth, temperature. Aggregate them directly with avg, min, max, percentiles:

SELECT avg(mem_used_pct) FROM host EVERY 5m LAST 24h

Counters only ever increase — bytes sent, requests served, operations completed. Their absolute value is rarely interesting; what you usually want is how much they grew over a window. In OQL you get that growth with max(col) - min(col)-style thinking, or by summing already-derived values. In the schema, counter columns are marked as such (see Tables) — for example everything in network (rx_bytes, tx_bytes) and disk (read_bytes, write_bytes) is a counter.

-- Network traffic per interface (counters): sum the byte counters per bucket
SELECT sum(rx_bytes), sum(tx_bytes) FROM network
BY interface EVERY 5m LAST 6h TOP 10

External-metric tables handle this for you

On the database / cache / mq virtual tables, each native metric column knows whether it is a gauge or a counter, and a bare column reduces accordingly over each bucket:

  • gaugeavg
  • monotonic counter → the window increase (max − min)

You can override that default by naming an explicit aggregate:

-- Default reduction: avg for the gauge, increase for the counter
SELECT postgresql.backends, postgresql.commits
FROM database WHERE engine = 'postgresql'
EVERY 1m LAST 1h

-- Explicit override: peak backends instead of the average
SELECT max(postgresql.backends)
FROM database WHERE engine = 'postgresql'
EVERY 1m LAST 1h

There is no rate() function. To get a per-second rate, take the counter increase over a bucket and divide by the bucket length yourself.