Dimensions, Measures & Filters
The most important concept in SemQL — and why it prevents the most common mistakes
Understanding the difference between dimensions and measures is the single most important thing to know about SemQL. Get this right and most queries just work.
Dimensions
A dimension is an attribute you slice, filter, or group by. Think: names, dates, categories, IDs, statuses.
-- Examples of dimensions
name
region
created_at
product_category
customer_idDimensions map to plain columns in your database, or sometimes to expressions (e.g., full_name might be CONCAT(first_name, ' ', last_name) under the hood — you don't need to know).
You use dimensions in:
SELECT(to show the attribute)WHERE(to filter before aggregation)GROUP BY(to group rows)ORDER BY(to sort)
Measures
A measure is a pre-defined, pre-aggregated metric. Think: revenue, user counts, order totals, conversion rates.
Measures are defined once by your data team in the semantic layer. They encode all the business logic: which columns to use, how to handle nulls, what to exclude, how to aggregate.
You use measures in:
SELECT(to show the metric)HAVING(to filter after aggregation)ORDER BY(to sort by the metric)
The Golden Rule: Never Aggregate a Measure
This is the most common SemQL mistake, and it produces silent, wrong results.
Measures are already aggregated. When you write total_revenue, it already contains a SUM() inside its definition. If you wrap it in another SUM(), you get double aggregation — which either errors out or returns garbage.
Same rule applies to COUNT, AVG, MIN, MAX, and any other aggregate:
How to tell if something is a measure: Check your semantic layer. Measures are tagged as such. When in doubt, if a field sounds like a business metric (revenue, count, rate, total, average), it's probably a measure.
WHERE vs. HAVING
Since dimensions and measures behave differently, they use different filter clauses:
What it filters
Dimensions (raw column values)
Measures (aggregated results)
When it runs
Before aggregation
After aggregation
Use for
status = 'active', date >= ...
total_revenue > 10000, order_count >= 5
A simple test: if you could put the condition in WHERE without a GROUP BY, it filters a dimension. If it only makes sense after grouping (because it involves an aggregated value), it belongs in HAVING.
Predefined Filters
Predefined filters are reusable WHERE conditions defined in the semantic layer. Instead of writing WHERE member_count > 0 everywhere, your data team defines it once as active_only. Anyone who uses organizations[active_only] gets exactly that condition — consistently.
Syntax
Filters go in the FROM or JOIN clause, in square brackets after the model name:
Filters are not just convenience — they're consistency
Imagine "enterprise customer" means annual_contract_value >= 50000 AND plan = 'enterprise'. Without predefined filters, every analyst writes that condition slightly differently. Some forget the plan check. Some use different thresholds.
With customers[enterprise], there's one definition. Every query, every agent, every analyst uses the same criteria.
Auto-applied model filters
Some models have filters that apply automatically to every query, without you having to specify them. These are guardrails set by your data team.
For example, if the transactions model has an auto-filter for status = 'completed', then FROM transactions always excludes incomplete transactions — even if you don't ask for it. You can't bypass them, and you shouldn't need to. They exist to prevent mistakes.
Filters don't belong in SELECT
This is a syntax error:
Quick Reference
Dimension
SELECT, WHERE, GROUP BY, ORDER BY
region, created_at, name
Measure
SELECT, HAVING, ORDER BY
total_revenue, user_count
Predefined filter
FROM, JOIN (in square brackets)
[active_only], [last_quarter]
Aggregate function on dimension
SELECT with GROUP BY
COUNT(order_id)
Aggregate function on measure
Never — already aggregated
—
Dimension filter
WHERE
WHERE region = 'EU'
Measure filter
HAVING
HAVING total_revenue > 0
Last updated