Side quest / AQL semantics
Order of operations of an AQL query
Source: Holistics docs: AQL order of operations. Tangible win: predict, line by line, when each part of an AQL query runs, and explain it in SQL terms.
Purpose of this lesson
Have you ever tried to debug "why does my filter not change this metric"?
You hit these pain points:
- You added
orders.country == 'US'infilters {}, but a "percent of total" metric did not move. You suspect a bug, but the metric is doing exactly what it should at a different step. - You wrote a window function like
previous()orrank()in a dimension and a downstream filter unexpectedly changed the ranks. - You explained to a customer "we filter, then we aggregate", but the actual answer is more nuanced and you cannot point at the rule.
This lesson solves it: walk the seven-step AQL pipeline in SQL terms, with diagrams, so you can name the step that controls each behavior and stop guessing.
SQL refresher: logical query order
SQL is written top-down (SELECT ... FROM ... WHERE ...) but executed in a different logical order. Most engines run the clauses like this:
FROMandJOIN: build the row set.WHERE: filter rows.GROUP BY: collapse rows into groups.HAVING: filter on aggregated values.SELECT: compute final columns (including window functions).ORDER BYandLIMIT: shape output.
Two filters, before and after aggregation. AQL has the same idea, with explicit names.
AQL pipeline in 7 steps
AQL step to SQL clause
| Step | AQL action | SQL analogue |
|---|---|---|
| 1 | Create model CTEs | FROM + JOIN (driven by relationships) |
| 2 | Apply query params | Template substitution before run |
| 3 | Execute dimensions, including window funcs | SELECT computed columns and window functions |
| 4 | Drop filters on excluded dimensions per metric | Per-metric WHERE rewrite (no SQL equivalent in one place) |
| 5 | Apply filters on dimensions | WHERE |
| 6 | Run aggregations and metric logic | GROUP BY with aggregate functions |
| 7 | Apply filters on measures or metrics | HAVING |
The big two: dimension filter vs metric filter
This is the rule that solves the most "why did my number not change" questions:
- Filter on a dimension runs at step 5, before aggregation. It changes what rows go into the measure.
- Filter on a measure or metric runs at step 7, after aggregation. It hides whole groups whose aggregate value did not pass the threshold.
Filter at step 5 (dimension)
explore {
measures {
revenue: order_items | sum(order_items.revenue),
}
dimensions {
countries.name,
}
filters {
countries.name == 'US',
}
}
SQL equivalent: WHERE countries.name = 'US'. US-only rows feed the sum.
Filter at step 7 (measure)
explore {
measures {
revenue: order_items | sum(order_items.revenue),
}
dimensions {
countries.name,
}
filters {
revenue > 1000000,
}
}
SQL equivalent: HAVING SUM(revenue) > 1000000. All countries aggregate; only those above the threshold survive.
Gotcha: step 4 drops filters when of_all excludes a dimension
of_all(model) or exclude(model) wants to "ignore that model when calculating". As a result, any filter that references the excluded dimension is dropped for that metric only. The dimension still appears in the result, but the metric is calculated as if the filter were not there.
metric percent_of_total {
definition: @aql revenue * 1.0 / (revenue | of_all(countries));;
}
explore {
measures { percent_of_total }
dimensions { countries.name }
filters { countries.name in ['US', 'UK'] }
}
What happens:
- Numerator
revenuerespects the country filter. - Denominator
revenue | of_all(countries)ignores the country filter, so it equals revenue across all countries. - Result: a "share of global total", not a "share of US+UK".
Two workarounds
Promote the condition into a metric of its own and filter on the metric at step 7. Step 7 sees the post-aggregation world.
Move the condition inside the metric via case() or where() so it is part of the aggregation, not an outer filter.
Window functions in dimensions: always at step 3
Functions like rank(), ntile(), previous() used inside dimensions { } run at step 3, before any dimension filter at step 5. That means a top-N filter on a rank does not change the ranks themselves; ranks are already computed across the full dataset.
explore {
dimensions {
countries.name,
rev_rank: rank(revenue) | over(),
}
measures { revenue }
filters {
rev_rank <= 5,
}
}
Reading: rev_rank is computed across every country at step 3. The filter then keeps only the rows where rev_rank is at most 5. You see the top 5 globally, not the top 5 among an already filtered subset.
Putting it together: read a query like the pipeline
- Identify the source model(s). That is steps 1 and 2.
- Read every dimension. Computed and window dimensions are evaluated up front (step 3).
- List filters. Mark each one: dimension or measure.
- Drop dimension filters that target excluded models in metrics that use
of_allorexclude(step 4). - Apply remaining dimension filters (step 5).
- Aggregate with measures and metrics (step 6).
- Apply remaining measure filters (step 7).
Retrieval practice
Read next
Primary source: Holistics docs: AQL order of operations. Related: Metric context, Level of detail.
Back to the spine
Return to Lesson 0007 - Relationships or the course map.