Skip to content

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

  1. Define metrics in dbt and reuse them in Dataface.
  2. Write shorter board YAML for standard metric queries.
  3. Change time grains or dimensions without rewriting SQL.
  4. 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:

pip install dataface[metricflow]

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:

dbt parse

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, not region, because the semantic model keys region off the order_id entity.
  • time_grain: month is Dataface's own field — it expands automatically to the reserved group-by name metric_time__month. Don't add a time dimension to dimensions: yourself; use time_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 to order_id__region. You write the bare column name; Dataface adds the entity.
  • x: metric_time__month — the reserved metric-time reference, which sets time_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_usd or active_users_7d.
  • Define common dimensions, such as region and customer_segment, in dbt.
  • Test metrics in dbt (mf query --metrics ...) before wiring them into a board.
  • Re-run dbt parse after any semantic-layer change — Dataface compiles against whatever manifest is on disk, stale or not.

Learn More