GraphQL GROUP BY Aggregate Queries
Every table gets a grouped-aggregate root field, <table>Aggregate, that
runs GROUP BY with count, sum, avg, min, and max on the server and
returns one row per group. It is the analytical companion to the row query:
totals, breakdowns, and count cards come from here, not from summing fetched
page rows on the client.
The field name, the groupable-column set, and the value operations all derive
from a single source of truth,
AggregateSurface,
and execute through
AggregateTableResolver.
The <table>Aggregate field
Section titled “The <table>Aggregate field”type Query { ordersAggregate( limit: Int offset: Int filter: TableFilterordersInput groupBy: [ordersEnum!] ): [orders_aggregate!]!}
type orders_aggregate { # one nullable group-key field per visible column status: String region: String # count of rows in the group _count: Int! # value op groups (present only when the table has numeric columns) _sum: orders_aggregateFields _avg: orders_aggregateFields _min: orders_aggregateFields _max: orders_aggregateFields}
type orders_aggregateFields { amount: Float quantity: Float}groupBytakes a list of the schema-derived column enum (<table>Enum), so a caller can never pass a name that is not a real, visible column._countis always available._sum/_avg/_min/_maxare only emitted when the table has at least one numeric column; each resolves to an object with oneFloatfield per numeric column. (min/maxare restricted to numeric columns in this release; date/string extrema are a deferred extension.)
Example
Section titled “Example”Count orders and total their amount, broken down by status and region:
{ ordersAggregate(groupBy: [status, region]) { status region _count _sum { amount } _avg { amount } }}Paging the groups
Section titled “Paging the groups”limit and offset page the group window, ordered by the groupBy columns
ascending so the pages are stable and never overlap:
{ ordersAggregate(groupBy: [status, region], limit: 50, offset: 50) { status region _count }}The window is bounded whether or not the caller asks for it. A groupBy on a
high-cardinality column — groupBy: [id] is one group per row — would
otherwise read the whole table through the aggregate surface:
- With no
limit, the aggregate returns at most 100 groups, the same default a row query takes. limitis clamped by the model’smax-query-rowsceiling (default 10000). A caller may narrow the window; alimitabove the ceiling, and the no-limit sentinellimit: -1, clamp back to it. A ceiling lower than 100 also lowers the default.- An aggregate with no
groupByreturns one whole-table row and takes no window.
Security: filters apply before grouping
Section titled “Security: filters apply before grouping”Rows excluded by the tenant-isolation and soft-delete filter transformers are
excluded from the aggregate. The resolver applies the same
transformer-derived filters as the row query before the GROUP BY, so a
count or sum can never include rows the caller cannot read. Column-level read
guards (IColumnReadGuard) are enforced against both groupBy columns and
_sum/_avg/_min/_max value columns — a denied column cannot be read
through the aggregate surface. Only the sub-fields the client selects under
each op group are aggregated and guarded: _sum { amount } neither computes
nor requires read access to any other numeric column, so a policy-denied
sibling column never blocks an aggregate that does not select it.
All identifiers are schema-derived and all values are parameterized; no user-supplied string is ever concatenated into the generated SQL. The surface is covered across SQL Server, PostgreSQL, MySQL, and SQLite.
Related
Section titled “Related”- Pivot / cross-tab queries — one output column per distinct value instead of one row per group.
- Chart panel and grid grouping — desktop workbench surfaces that consume this field.