9.4 Aggregation and analytics
Blocking stages that summarize rows (group, bucket, pivot) or compute across rows (window, fill).
All run within the query’s memory budget and spill to disk past it.
group
Definition. Aggregates rows by keys: one output row per distinct key.
Compound form
group
| by <key> [as <alias>] [, <expression> as <alias>]*
| <measure> …
| end
| Measure | Result | Default name |
|---|---|---|
sum <value> | Sum of the numbers (integer while exact); the field is left out when the group has no number | <field> |
avg <value> | Mean of the numbers, null when none | avg_<field> |
min <value>, max <value> | Smallest / largest non-null value; values must be comparable | min_<field>, max_<field> |
first <value>, last <value> | Value on the first / last row of the group, in input order (null when missing) | first_<field>, last_<field> |
first <value> by <order>, last <value> by <order> | Value on the row of smallest / largest <order>, whatever the input order; rows without an order are skipped | first_<field>, last_<field> |
count | Number of rows | count |
count <value> | Rows where the value is present and not null | count_<field> |
list <value> | Array of the values neither missing nor null, in input order | list_<field> |
median <value> | Middle value of the numbers | median_<field> |
percentile <value> <fraction> | Value at a fraction (0 to 1), linear between the two closest values | percentile_<field> |
stddev <value> | Sample standard deviation | stddev_<field> |
Every measure accepts [where <predicate>] and [as <alias>]:
| sum amount where category in ["Sales", "Services"] as revenue
| count where amount < 0 as credits
Behavior
byis required, exactly once, and may list several keys. A key may be an expression withas(by date_format(date, "%Y-%m") as month).- Measures appear zero or more times, in any order. With none, the group emits
count. - A measure’s value may be an expression with
as(sum price * qty as revenue). whereon a measure counts the value asnullon the rows it rejects: every measure ignores them, andcount where pcounts the kept rows.first/lastwithwhereneedby <order>.median,percentileandstddevkeep the group’s numbers (alist) and reduce them at the end.- A collection is read in storage order, which a replaced row may change: use
first … by <order>/last … by <order>for a defined order. - Output rows come in key order.
Compact form
group <key> [as <alias>] -- count per key
group <key> [as <alias>], <item> [as <alias>]… -- numbers summed, the rest keys
- One item: count per key.
- Several items: the first is a key. Each later item is summed when every value it takes is a number, and is a key otherwise. With no summed item, the group counts.
- What an item holds is known before the scan, from the storage’s per-field kinds, through the
where,limit,skip,sortandderivestages before thegroup. An arithmetic or numeric function expression is a number; a comparison, a text or date function, a text or boolean field is a key. - When that cannot be known (another stage before, a nested path,
at/get), every item but the last is a key and the last is summed. - A numeric code (an integer client number) is summed too: group by it with the compound form, or
to_string(code) as code.
Examples
on orders | group | by region | sum qty as units | avg price | count as orders | end
{"avg_price":39.833333333333336,"orders":3,"region":"EU","units":8}
{"avg_price":119.83333333333333,"orders":3,"region":"US","units":8}
on orders | group status -- {"count":4,"status":"paid"} …
on orders | group region, qty -- {"qty":8,"region":"EU"} …
on orders | group region as zone, month, qty as units
on orders | group region, qty, price * qty as revenue, status
on ledger
| group
| by entity, account
| last balance by month as closing
| sum amount where category in ["Sales", "Services"] as revenue
| count where amount < 0 as credits
| sum amount * rate as converted
| end
on orders | group | by user | list product as products | end
on orders | group | by region | median price | percentile price 0.9 as p90 | stddev price | end
Patterns
- HAVING: filter after the group:
group | by user | sum qty as units | end | where units > 3. - Grand total: a constant key,
derive all = "total" | group | by all | sum amount | end. - Top N groups:
group … | sort revenue desc | limit 10(a bounded group then Top-N).
Errors and limits
bymissing or repeated.min/maxon values of different kinds.group | limit Nkeeps only the N smallest live keys.
See also pivot, bucket, count, aggregate functions.
bucket
Definition. Counts rows per value range.
Syntax
bucket <expression> by [<b0>, <b1>, …] [default <value>] [as <alias>]
Behavior. Each row falls in the range [b(i), b(i+1)) named by its lower bound; rows outside every range go to
default (or null). Bounds must be strictly ascending. The alias defaults to bucket. It is shorthand for
derive <alias> = bucket(<expression>, [...], <default>) | group <alias>.
Example
on users | bucket age by [0, 18, 40] default "40+" as band
{"band":0,"count":1}
{"band":18,"count":2}
{"band":"40+","count":1}
See also the bucket function, to bucket without grouping.
pivot
Definition. Cross-tabulates rows: one output row per row key, one field per column value.
Syntax
pivot
| rows <field> | <expression> as <alias> [, …]
| columns <field> | <expression> as <alias> [, …]
| values <field> [as <alias>] | <expression> as <alias> [, …]
| aggregate sum | count | avg | average | min | minimum | max | maximum | first | last
| end
Behavior
- Each directive appears exactly once.
valuestakes one or more measures, written likeselectitems. With one measure, each cell is named after its column value; with several,<column>.<name>(names must be unique).- A row is named after its alias; a column’s alias is required but unused (its values name the cells).
- Expressions in
rows,columnsorvaluesare computed by a hiddenderivebefore the pivot. - Cells aggregate as
groupdoes: sums stay integers when exact (a cell with no number is0), non-numbers are ignored bysum/avg, missing values skipped by every aggregate butcount. - A cell with no row is absent: read it with
?after the pivot. - Stages may follow
end.
Examples
on orders
| pivot | rows region | columns month | values qty | aggregate sum | end
{"2026-01":7,"2026-02":1,"region":"EU"}
{"2026-01":7,"2026-02":1,"region":"US"}
on sales
| pivot | rows substr(period, 0, 4) as year | columns kind | values amount | aggregate sum | end
| derive margin = coalesce(sales?, 0) - coalesce(costs?, 0)
See also group.
window
Definition. Adds to every row values computed over its ordered partition: previous and next values, running sums, ranks.
Syntax
window
| partition by <field> [, <field>]*
| order by <field> [asc | desc] [, …]
| lag <field> [<rows>] [as <alias>]
| lead <field> [<rows>] [as <alias>]
| cumsum <field> [as <alias>]
| first <field> [as <alias>]
| last <field> [as <alias>]
| row_number [as <alias>]
| end
| Function | Value on each row | Default name |
|---|---|---|
lag <field> [<rows>] | The field <rows> rows earlier (default 1); null before the first | lag_<field> |
lead <field> [<rows>] | The field <rows> rows later; null after the last | lead_<field> |
cumsum <field> | Running sum up to this row (integer while exact, non-numbers ignored); null before the first number | cumsum_<field> |
first <field>, last <field> | The field on the partition’s first / last row | first_<field>, last_<field> |
row_number | 1, 2, 3… within the partition | row_number |
Behavior
order byis required, once;partition byis optional (the whole input is one partition without it); at least one function.- Partition keys compare as
sortcompares them (1and1.0share a partition). - Functions run in order: one may read a field an earlier one added.
- Rows come out sorted by partition, then order, keeping all their fields.
- It is a stable sort (it spills) then a pass holding one partition at a time.
Example
on ledger
| window
| partition by entity, account, fiscal_year
| order by month
| lag balance as previous
| cumsum movement as ytd
| row_number as n
| end
| derive variation = balance - coalesce(previous, 0)
Errors and limits
a window partition exceeds the governed query working set: partition more finely.- Partition and order keys are field paths:
derivecomputed keys first. - Top level or a
unionbranch only.
fill
Definition. Adds, in each partition, a row for every value of a grid that no row holds: complete missing months, missing steps.
Syntax
fill <field> from <start> to <end> step <step> [by <field> [, <field>]*] [with <field> = <value> [, …]]
Behavior
- The grid is
start,start + step, … up toend, inclusive. stepis a positive number, or a date unit for date bounds (step "month", units as indate_add). A date grid is written asstartis:"2026-01"gives"2026-02","2026-03"…; a full date gives full dates; epoch milliseconds give milliseconds.- A new row holds the partition’s
byfields, the grid value and thewithvalues; other fields are missing. - Rows already there pass unchanged, values off the grid included. Output is sorted by the
byfields then<field>. - An empty input stays empty: a partition exists only through its rows.
- Bounds may be parameters (
from $start to $end); at most 100,000 grid values.
Example
on ledger
| fill month from $opening to $closing step "month" by entity, account with balance = 0, movement = 0
| window | partition by entity, account | order by month | lag balance 12 as last_year | end
Limits. <field> and by are field paths; top level or a union branch.