The semantic (metrics) layer
Why is a metric a computation and not a value, and what does a semantic layer really enforce?
The constraint is categorical, and it is the reason this mechanism exists at all: a metric is a computation, not a value.
"Active customer" is a predicate (what counts as active), plus a window (the last 30 days? the calendar month?), plus a deduplication rule (one person who ordered three times counts once). It cannot be stored as a column, because it is only defined relative to a grouping and a time frame, and both are chosen by the person asking.
Where does the definition live?
Therefore it lives somewhere. If it lives nowhere central, it lives in every consumer's query. And because the ambiguity is in the specification and not in the code, N independent implementations produce close to N distinct definitions. They do not differ randomly. They differ on the edge cases nobody wrote down:
- Are refunded orders counted?
- Are test accounts and employee accounts counted?
- Which time zone defines "September"?
- Is a half-finished period counted?
-- Marketing: any order in September (UTC), refunds included
SELECT COUNT(DISTINCT customer_id) FROM orders
WHERE order_ts >= '2025-09-01' AND order_ts < '2025-10-01';
-- Finance: paid, not refunded, test accounts excluded, shop time zone
SELECT COUNT(DISTINCT customer_id) FROM orders
WHERE status = 'paid' AND NOT is_refunded AND NOT is_test_account
AND (order_ts AT TIME ZONE 'Europe/Berlin') >= '2025-09-01'
AND (order_ts AT TIME ZONE 'Europe/Berlin') < '2025-10-01';Both look right. They are different metrics with the same name.
This is the law this mechanism contributes, and it is not about laziness:
What the layer really does
A semantic layer is advertised as reuse. Its real function is the automated enforcement of the two rules a hand-written query forgets:
- A semi-additive measure must not be summed over time (Additivity comes first).
- A join path that crosses a many-to-many needs an allocation (Many-to-many).
So a semantic layer is a fan-trap and additivity guard with a reuse benefit attached.
# A metric as code (illustrative format)
metric: active_customers
type: count_distinct
of: orders.customer_id
filter: status = 'paid' AND NOT is_refunded AND NOT is_test_account
time_dimension: orders.order_ts
timezone: Europe/Berlin
owner: finance-dataMetric definitions as code buy one property that matters more than version history: semantics become diffable. A change in "net revenue" appears as a reviewable diff, not as a step in a chart that somebody notices in a meeting.
The honest costs
- The layer becomes a query compiler. It inherits every compiler concern from query optimization: a semantic layer that cannot push predicates down (apply filters as early as possible) is a correctness win and a performance disaster.
- It concentrates coordination: every definitional dispute now routes through one artifact and one set of reviewers. That is the point of it, and it is also the bottleneck.