Aggregates — AQL
Aggregate select
Section titled “Aggregate select”select count(User);SELECT COUNT(*) FROM ( SELECT 1 FROM "user" u) _agg;With a filter:
select count(User filter .active = true);SELECT COUNT(*) FROM ( SELECT 1 FROM "user" u WHERE u.active = true) _agg;As a scalar subquery
Section titled “As a scalar subquery”An aggregate can also be wrapped in parentheses and used as a scalar operand
anywhere an expression is accepted — inside a filter, or on the right-hand
side of an update set assignment. It compiles to the same SELECT COUNT(*)
wrapped in parentheses, and its filter may correlate to the outer row.
multi select User { id, email }filter (select count(Post filter .author.id = User.id)) > 0;SELECT u.id AS id, u.email AS emailFROM "user" uWHERE (SELECT COUNT(*) FROM ( SELECT 1 FROM "post" p WHERE p.author = u.id) _agg) > 0;The inner filter .author.id = User.id references the outer alias (u.id), so
the count is evaluated per user. The same form works as an assignment value —
set { has_posts := (select count(Post filter .author.id = User.id)) > 0 }.
Note: an aggregate subquery is only valid as an expression operand. It is not accepted as a computed shape field value (
{ n := (select count(...)) }).
Aggregate shape — many aggregates in one scan
Section titled “Aggregate shape — many aggregates in one scan”A select whose shape fields are aggregates computes several aggregates over the
same set in a single pass. Each field is name := <agg>(.column) with an optional
per-field filter, and the select’s own filter (after the shape, as usual) is the
shared condition applied to every field:
select Transaction { success_debit := sum(.amount) filter .type = TransactionType.Debit and .status = TransactionStatus.Successful, pending_debit := sum(.amount) filter .type = TransactionType.Debit and .status = TransactionStatus.Pending, success_credit := sum(.amount) filter .type = TransactionType.Credit and .status = TransactionStatus.Successful, pending_credit := sum(.amount) filter .type = TransactionType.Credit and .status = TransactionStatus.Pending,}filter (.sender_id = $api_key_id and .sender_entity = TransactionActorEntity.ApiKey) or (.reciever_id = $api_key_id and .reciever_entity = TransactionActorEntity.ApiKey);Each field lowers to a Postgres FILTER (WHERE …)
aggregate, so the whole query is one scan — no correlated subqueries:
SELECT SUM(t.amount) FILTER (WHERE t.type = 'Debit' AND t.status = 'Successful') AS success_debit, SUM(t.amount) FILTER (WHERE t.type = 'Debit' AND t.status = 'Pending') AS pending_debit, SUM(t.amount) FILTER (WHERE t.type = 'Credit' AND t.status = 'Successful') AS success_credit, SUM(t.amount) FILTER (WHERE t.type = 'Credit' AND t.status = 'Pending') AS pending_creditFROM "transaction" tWHERE (t.sender_id = $1 AND t.sender_entity = 'ApiKey') OR (t.reciever_id = $1 AND t.reciever_entity = 'ApiKey');The result is a single row (one *Row in generated code); multi, order by,
limit, and offset are not allowed.
- Aggregate functions:
sum,avg,min,max,count.count()(no argument) isCOUNT(*); the others take an argument expression (such as.column, or a math / function expression likemin(haversine(.loc.lat, .loc.lon, $target_lat, $target_lon))). The per-fieldfilteris optional. - A shape is an aggregate shape as soon as one field is an aggregate; every field must then be an aggregate — mixing aggregates with plain row fields requires a Group By clause.
- Result types. Aggregate fields are nullable (an aggregate over zero rows is
NULL).countisint64;min/maxkeep the column’s type.sumandavgchange type in Postgres (e.g.sumof abigintcolumn isnumeric), so add a cast to pin the generated type —sum(.amount)<int64>— otherwise the field is typed asanyand code generation warns.