Praxsuite

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

count

Number of rows

sum

Sum of numeric values

avg

Average of numeric values

min

Minimum value

max

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

field

yes

Column to aggregate or group by

fn

no

Aggregate function. Omit it for a dimension column.

alias

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

  1. FROM — pick the table

  2. WHERE — filter individual rows

  3. GROUP BY — group what survived

  4. Aggregate functions — compute per group

  5. HAVING — filter the groups

  6. ORDER BY — sort them

  7. LIMIT / 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

AllowAggregations

table scope

any groupBy or aggregate on that table returns 403 SCOPE_VIOLATION

CanAggregate

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.