MetricFlow¶
MetricFlow is dbt's semantic-layer query engine. It lets Dataface ask for metrics and dimensions by name instead of embedding SQL in every board.
Use it when your dbt project already defines shared metrics and you want boards to reuse those definitions.
Why Use MetricFlow?¶
Traditionally, BI tools require you to write SQL queries for every chart, or model data specifically for the tool. This leads to: - Inconsistent metrics: "Revenue" might be calculated differently in two different charts. - Maintenance headaches: Changing a metric definition requires updating SQL in 20 different places. - Rigid querying: Hard to drill down or change time grains on the fly.
MetricFlow solves this by letting you define metrics once in your dbt project. Dataface then asks for "revenue by month," and MetricFlow generates the SQL.
Benefits¶
- Define metrics in dbt and reuse them in Dataface.
- Write shorter board YAML for standard metric queries.
- Change time grains or dimensions without rewriting SQL.
- Keep metric definitions consistent across boards.
How It Works¶
A metrics:/dimensions: query lowers to plain SQL at compile time: Dataface
calls MetricFlow's own compiler (MetricFlowEngine.explain) against your dbt
project's semantic manifest and gets back a finished SELECT statement. That SQL
then runs through the query's dbt_profile source exactly like a hand-written SQL
query — caching, variable substitution in the rest of the board, and the
compile-time SQL guard all apply for free. There is no MetricFlow call at render
or execute time, and no live connection to MetricFlow needed in production.
One consequence of compiling at build time: MetricFlow bakes literal filters
directly into the generated SQL at compile time, while {{ variable }} values
only exist at render time. The where: key handles both — see
Filters and Variables below.
Install the optional extra to compile metricflow queries:
1. Define Metrics in dbt¶
First, define metrics in your dbt project.
# dbt_project.yml or schema.yml metrics: - name: total_revenue type: simple type_params: measure: revenue
See dbt's MetricFlow documentation for details on defining metrics.
Then generate the semantic manifest MetricFlow compiles against:
This writes target/semantic_manifest.json, which Dataface reads at compile
time. Re-run dbt parse whenever metrics, dimensions, or entities change —
Dataface does not invoke dbt itself, it only reads the manifest dbt already
produces. A dbt Cloud–hosted project (no local dbt_project.yml/manifest on
disk) is not supported by this source; run dbt parse locally or in CI and
ship the resulting target/ directory alongside the dbt project.
2. Configure the Source¶
A metricflow query's source: is an ordinary type: dbt_profile source — the
same source type plain dbt-model SQL queries use. There is no separate
type: metricflow source config.
# dataface.yml sources: analytics: type: dbt_profile profile: my_dbt_project target: dev
See Sources for profile resolution and connection setup.
3. Reference in Dataface¶
Reference the metric by name. No SELECT, FROM, or GROUP BY needed.
# dashboard.yml queries: revenue: source: analytics metrics: [total_revenue] dimensions: [order_id__region] time_grain: month
Dimension naming¶
Group-by names follow MetricFlow's own naming convention, not the bare column name:
- Non-time dimensions are entity-qualified:
order_id__region, notregion, because the semantic model keysregionoff theorder_identity. time_grain: monthis Dataface's own field — it expands automatically to the reserved group-by namemetric_time__month. Don't add a time dimension todimensions:yourself; usetime_grain:.
Chart x:/color:/etc. fields reference these same MetricFlow-native names
(metric_time__month, order_id__region), since they are the columns the
lowered SQL actually selects.
model: chart sugar¶
You don't have to write the queries: block at all. A chart can name its
semantic model directly with model: <source>.<semantic_model> and reference
fields by their bare manifest names — Dataface reads the semantic manifest and
infers each field's role, so you never restate the metric-vs-dimension split:
# dashboard.yml — same result as the explicit query above charts: revenue_by_region: type: bar model: analytics.orders x: metric_time__month y: total_revenue color: region
Role inference from the manifest:
y: total_revenue— a metric name, kept as-is.color: region— a categorical dimension, entity-qualified automatically toorder_id__region. You write the bare column name; Dataface adds the entity.x: metric_time__month— the reserved metric-time reference, which setstime_grain: month.
A channel field that is not a metric, a dimension, or metric_time__<grain> in
the named semantic model is a compile error — Dataface never silently drops it.
model: is mutually exclusive with query:.
Flexible Time Grains¶
Want to see the same metric by week instead of month? Just change one line — each grain recompiles to its own SQL.
queries: revenue_weekly: source: analytics metrics: [total_revenue] dimensions: [order_id__region] time_grain: week
Multi-Metric Analysis¶
Combine multiple metrics in a single query to see correlations.
queries: overview: source: analytics metrics: [total_revenue, order_count, average_order_value] time_grain: month
Filters and Variables¶
Metricflow queries take a where: list of SQL predicates over the query's
MetricFlow group-by names. Each predicate lands in one of three tiers:
variables: region_pick: options: static: [east, west, north] date_range: input: daterange queries: revenue: type: metricflow metrics: [total_revenue] dimensions: [order_id__region] time_grain: month where: - "customer__segment = 'enterprise'" # literal — baked at compile - "{{ filter('order_id__region', region_pick) }}" # variable — applied per render - "{{ filter_date_range('metric_time__month', date_range) }}"
Literal predicates (no {{ }}) are translated into MetricFlow's native
where-constraints and baked into the generated SQL at compile time. Because
MetricFlow applies them pre-aggregation with full semantic-graph awareness,
they may reference any dimension — selected or not.
Variable predicates (containing {{ }}) are applied per render: the baked
SQL is wrapped in SELECT * FROM (...) WHERE <predicate>, and the predicate's
Jinja resolves through the same parameterized path every sql: query uses —
values bind as query parameters, and the
filter() helper makes an unset
picker mean "unfiltered". This is exact, not approximate: filtering on a
selected group-by dimension commutes with aggregation (each group's value
is computed only from that group's rows), so post-filtering equals
pre-filtering. For the same reason, a variable predicate on a dimension that is
not selected cannot be applied exactly — it fails to compile with two
rewrites: add the dimension to dimensions: (the filter becomes exact), or use
a sql: query.
Non-commuting metrics refuse where: entirely. Cumulative, conversion, and
offset-window metrics compute each group's value from rows outside the group
— a running total's March value includes January's rows — so no filter
placement is exact. Queries over those metric types reject where: at compile;
write the metric's SQL in a sql: query against the same dbt_profile source
instead.
Best Practices¶
To get the most out of this pairing:
- Name metrics clearly in dbt, such as
total_revenue_usdoractive_users_7d. - Define common dimensions, such as
regionandcustomer_segment, in dbt. - Test metrics in dbt (
mf query --metrics ...) before wiring them into a board. - Re-run
dbt parseafter any semantic-layer change — Dataface compiles against whatever manifest is on disk, stale or not.