Aggregations
Vincent Depassier · August 29, 2026
PraxQL — Aggregations
PraxQL supports GROUP BY, HAVING and aggregate functions in the same query body. There is no separate endpoint and no serverless function to write.
When to reach for it
Aggregate when you want a number about a set of rows rather than the rows themselves: revenue by customer, orders per status, average ticket by region, the largest order, the earliest date.
If you want the rows and the number, that is two queries. See the last section.
The five functions
Function | Description |
| Number of rows |
| Sum of numeric values |
| Average of numeric values |
| Minimum value |
| Maximum value |
Function names are case-insensitive. Anything outside this list returns 400 INVALID_QUERY.
Aggregate select
In an aggregate query every selected item is an object, not a bare string:
Field | Required | Description |
| yes | Column to aggregate or group by |
| no | Aggregate function. Omit it for a dimension column. |
| no | Name for the result column in the response |
"select": [
{ "field": "Region", "alias": "Region" },
{ "field": "Total", "fn": "sum", "alias": "TotalRevenue" },
{ "field": "Id", "fn": "count", "alias": "OrderCount" },
{ "field": "Total", "fn": "avg", "alias": "AvgOrderValue" },
{ "field": "Total", "fn": "max", "alias": "LargestOrder" }
]The same column can appear several times with different functions, as Total does here.
groupBy
The columns to group by. Every item in select without a fn must appear here — that is a SQL rule, not a PraxQL one, and breaking it is the most common cause of a rejected aggregate query.
"groupBy": ["Region"]having
Filters applied after grouping. Same condition syntax as where, plus the fn field so you can filter on an aggregate:
"having": [
{ "field": "Total", "fn": "sum", "op": "gt", "value": 10000 }
]Complete example
Revenue by region for orders after January 2026, keeping only regions above 10,000:
{
"refs": {
"Orders": "b2c3d4e5-f6a7-8901-bcde-f12345678901"
},
"query": {
"from": "Orders",
"select": [
{ "field": "Region", "alias": "Region" },
{ "field": "Total", "fn": "sum", "alias": "TotalRevenue" },
{ "field": "Id", "fn": "count", "alias": "OrderCount" },
{ "field": "Total", "fn": "avg", "alias": "AvgOrderValue" }
],
"where": [
{ "field": "Date", "op": "gte", "value": "2026-01-01" }
],
"groupBy": ["Region"],
"having": [
{ "field": "Total", "fn": "sum", "op": "gt", "value": 10000 }
],
"orderBy": [
{ "field": "TotalRevenue", "dir": "desc" }
],
"limit": 20
}
}{
"data": [
{ "Region": "EU", "TotalRevenue": 485000.00, "OrderCount": 312, "AvgOrderValue": 1554.49 },
{ "Region": "US", "TotalRevenue": 392000.00, "OrderCount": 280, "AvgOrderValue": 1400.00 },
{ "Region": "LATAM", "TotalRevenue": 98500.00, "OrderCount": 127, "AvgOrderValue": 775.59 }
],
"meta": { "count": 3, "limit": 20, "offset": 0, "total": null, "durationMs": 12 }
}The order things happen in
FROM— pick the tableWHERE— filter individual rowsGROUP BY— group what survivedAggregate functions — compute per group
HAVING— filter the groupsORDER BY— sort themLIMIT/OFFSET— paginate
The practical consequence: where filters rows before grouping, having filters groups after aggregating. "Regions whose completed orders total more than 10,000" is a where on status plus a having on the sum — putting the status test in having asks a different question and usually returns nothing.
Permissions
Aggregation needs two flags, and both default to `false`:
Flag | Where | Without it |
| table scope | any |
| column scope | that specific column cannot be aggregated |
So a brand-new key cannot aggregate anything until you turn it on. That is intentional: an aggregate can reveal the shape of data a caller is not allowed to read row by row.
Aggregations and relations
Aggregates do not run inside a relation sub-query. You can aggregate the primary table, or fetch related rows and aggregate them client-side.
When you need totals and detail rows, send two queries. It is the same round trips as one combined query would have cost internally, and each half stays cacheable on its own.