Semantic layer¶
The semantic layer is ZLens's shared business vocabulary. It's a superset of dbt's MetricFlow — ZLens imports dbt's semantic models as-is and lets you extend them with DQL-local definitions.
Object types¶
| Object | Purpose | Example |
|---|---|---|
| Metric | A measure with aggregation | sum(orders.amount) |
| Dimension | An attribute to slice by | customers.segment |
| Hierarchy | An ordered drill path | year → quarter → month → day |
| Cube | A pre-joined fact+dim set | orders_by_customer_region |
| Segment | A named boolean filter | active_customers |
YAML shape (DQL-local)¶
# semantic/finance.yaml
metrics:
- name: revenue
label: Revenue
description: Gross revenue
type: sum
sql: amount
table: orders
domain: finance
dimensions:
- name: segment
label: Customer segment
type: string
sql: segment
table: customers
hierarchies:
- name: time
levels:
- { name: year, label: Year }
- { name: quarter, label: Quarter }
- { name: month, label: Month }
- { name: day, label: Day }
dbt import¶
ZLens reads dbt metrics from target/semantic_manifest.json. After you
change them, run dbt parse, then npm run sync. See
Import a dbt project. Imported metrics and
dimensions appear in the Semantic tab alongside DQL-local ones.
Use a metric in a notebook¶
In a notebook, click the Semantic tab in the sidebar. Every metric is listed by its business name. Tick a metric: only the dimensions it can safely use appear (Governed dimensions · N available). Tick a dimension and click Preview & run. Then click Add to cell to add a semantic DQL cell.
In the saved query editor, Semantic picker builds a saved query from the same metric.
If the tab says No semantic layer imported yet., run dbt parse, then
npm run sync.
Which engine runs a metric¶
| Project | Engine | Runs |
|---|---|---|
| dbt project with MetricFlow installed, or dbt Cloud configured | MetricFlow / dbt Cloud | every dbt metric type |
| Everything else, including warehouse-first projects with no dbt | DQL native compiler | simple metrics, ratios of two simple metrics, and derived metrics that do arithmetic over simple metrics with no offset window |
An mf executable on the PATH does not make a project a MetricFlow project:
without a dbt_project.yml (or a dbt setting in dql.config.json) the native
compiler serves it. When the native compiler cannot run a metric it says why
(for example "revenue_growth_mom is a derived metric; the DQL native semantic
engine runs a derived metric only when its expression is arithmetic over simple
metrics with no offset window") instead of asking for dbt parse.
Pick the engine in Settings → Project & dbt → Semantic execution
adapters. Preferred runtime is Auto (recommended), dbt Cloud,
Local MetricFlow or Native (DQL). The engine in use is marked
ACTIVE. A metric the engine cannot run is greyed out in the Semantic
tab, and hovering it says why. Trust & Steps on a result names the
engine, for example Compiled through native.
Derived metrics¶
A derived metric such as average_order_value
(lifetime_spend_pretax / count_lifetime_orders) runs natively when every
input is a simple metric and none has an offset_window or offset_to_grain.
Each input is summed at the requested grain first, and the expression is then
evaluated once per output row, as MetricFlow does. The expression may use
+ - * /, parentheses, numbers, and NULLIF, COALESCE, ROUND and ABS.
Anything else, such as a prior-period comparison, needs MetricFlow or dbt Cloud.
Ratio metrics¶
A ratio divides two simple metrics, as MetricFlow does: both sides are
aggregated at the requested dimensions and time grain, then divided once per
row. loss_ratio = incurred_losses / written_premium is SUM(losses) / SUM(premium)
for each line of business, never an average of per-row ratios. Sides on
different fact tables are aggregated separately and joined on the grain, so
neither fact multiplies the other. A zero denominator gives NULL. A filter on
the numerator or denominator (numerator: {name: incurred_losses, filter: ...})
applies to that side only. A side that is itself a ratio, derived, or
cumulative metric needs MetricFlow.
Joins¶
The native compiler joins along dbt entities the way MetricFlow does:
- Foreign → primary (many-to-one): a claim amount's
claimjoins the claims model. A metric's ownfiltermay read a column of any model reached this way; the model is joined for it. - Primary/unique ↔ primary/unique (one-to-one): two models keyed by the
same entity join on that key. A subtype table such as
loss_payment, one row per claim amount that is a loss payment, filters or groups claim amounts withoutclaim_amount_id in (from loss_payment)in the measure. A dimension that reads no column (has_loss_payment: 1) is the value when the subtype row exists and NULL when it does not. - Composite keys: an entity
exprmay be an expression (policy_id || '-' || coverage_code) or a column list (policy_id, coverage_code). Each column is qualified on its own side. - Model-level
primary_entity:names a keyless model (a bridge or a fact with no single-column grain). Its dimensions are addressed<primary_entity>__<dimension>, exactly as MetricFlow addresses them, andzlens semantic import dbtwrites that name into the imported YAML (qualified_name), so semantic models need not be named after their entity.
Bridges: why claim money cannot reach the agent¶
The compiler never walks an entity join backwards. Claim amounts reach the
policy (claim amount → claim → policy), but policy_party_role holds several
rows per policy (the holder and the agent), so going on to a party role would
repeat every claim amount once per role. MetricFlow refuses the same path, and
also stops at two hops. The compiler refuses the block and names the step:
party_role lives on policy_party_role, which incurred_losses reaches only through insurance_policies → policy_party_role: one insurance_policies row has many policy_party_role rows, ...
Model the rows you need as one row per policy — for example a policy_agent
dbt model (where Party_Role_Code = 'AG') whose entity policy is declared
type: unique, with primary_entity: policy_agent at model level. Keeping
insurance_policies the only primary model for policy keeps it the target
of every foreign policy; policy_agent then joins it one-to-one, and a claim
metric groups by the agent in both engines (claim__policy__agent_party_id is
two hops for MetricFlow).
In the sample project, one order has many order items. So when you pick
Orders in the Semantic tab, no order-item fields are offered. If you
force one in a cell, the run stops with Could not compose SQL for semantic
block metrics and names the fix: model one row per order, keyed
type: unique. Ask has the same guard: if a join would multiply rows, it
stops and runs nothing.
CLI¶
zlens semantic list # list everything
zlens semantic validate # check all refs resolve
zlens semantic query 'revenue by segment in 2024' # NL → SQL preview
zlens semantic pull # re-sync from dbt
Referencing from cells¶
// In a DQL cell
@metric("revenue") by @dim("customer.segment") for 2024
The compiler resolves refs, joins the needed tables, and emits SQL for the target warehouse dialect.


