"X per Y per Z"
Two "pers" means two levels. The innermost is the group() dimension. The outer is the explore dimension.
AQLearn 2.7 / Pattern
Source lesson: AQLearn 2.7: Nested Aggregations (1). Tangible win: write any "aggregate of aggregates" metric using one fixed pipeline shape.
Have you ever tried to answer "what is the highest number of users among all cities of each country"?
You hit these pain points:
max(count(users.id)) or count(users.id) | max() and get errors or wrong numbers.This lesson solves it: name the inner and outer parts of the question, then map them onto the fixed AQL shape group + select + outer aggregate. Same pattern works for highest, lowest, average, sum, and median of per-group values.
Pick a sentence with two aggregate words. Color the inner part one color and the outer part another:
"Highest number of users among all cities of each country."
| Level | Aggregate (verb) | Dimension (what to group by) |
|---|---|---|
| Inner (blue) | count of users | city |
| Outer (orange) | max of the inner counts | country (set by the explore) |
metric my_metric =
source_table
| group(inner_dimension)
| select(inner_aggregation)
| outer_aggregate();
source_table: the model that holds the rows you are counting (often the row source of the inner aggregate).group(inner_dimension): one row per inner group (city, month, customer, etc.).select(inner_aggregation): per-group value (count, sum, avg).outer_aggregate(): a scalar function that collapses those per-group values into one (max, avg, min, etc.). The explore dimensions partition this outer aggregate.metric my_metric =
users
| group(cities.id)
| select(users | count(users.id))
;
explore {
measures { my_metric }
}
Each result row currently holds a list of inner counts, one per city. With no outer dimension, all cities collapse into a single list.
Example output: [89, 88, 84, 97, 87, 95, 103, 101, 83, 91, 74] (the per-city counts).
Add an explore dimension so each row of the result holds only the per-city counts within that outer group.
explore {
measures { my_metric }
dimensions { countries.name }
}
Australia has two cities, so its row now holds two counts: [103, 89]. Vietnam holds [94, 83]. And so on.
metric my_metric =
users
| group(cities.id)
| select(users | count(users.id))
| max()
;
Now each row is one number. Australia: 103. Vietnam: 94. The original question is answered.
Want "average per country" instead of "max per country"? Change max() to avg(). The shape of the metric stays the same.
... | select(users | count(users.id)) | avg();
Want "average per continent"? Keep the metric, change the explore dimension.
explore {
measures { my_metric }
dimensions { countries.continent_name }
}
The metric stayed the same. The explore did the rest.
A subquery emits the inner table; the outer query aggregates again with the outer dimension as the partition.
select
countries.name
, max(per_city_count) as max_per_city_count
from (
select
countries.name
, cities.id
, count(users.id) as per_city_count
from users
left join cities on users.city_id = cities.id
left join countries on cities.country_code = countries.code
group by countries.name, cities.id
) t
group by countries.name
Two "pers" means two levels. The innermost is the group() dimension. The outer is the explore dimension.
Any superlative or summary word over a count or average is an outer aggregate. Plug it in after select().
The outer aggregate runs at step 6 of the AQL pipeline. The explore dimensions partition that step.
Inner group: cities.id. Outer dimension: countries.name or countries.continent_name. Source: users.
users (source) is grouped by cities.id (inner) and rolled up by countries.name or continent_name (outer).
countries.name as a dimension. Read Australia's row as [103, 89].| max() to the metric. Now Australia is 103.max() with avg() and then change the dimension to countries.continent_name.Next: Nested aggregations (2). More patterns, including questions with three "pers" stacked.