In the Semantic Layer you never write a join. You give two semantic models an entity with the same name, and MetricFlow joins them whenever a query needs a dimension from the other model. The rules that decide whether it may do so come from the entity types.
Copy and paste
models:
- name: orders
semantic_model:
enabled: true
agg_time_dimension: order_date
columns:
- name: order_id
entity: {type: primary, name: order}
- name: customer_id
entity: {type: foreign, name: customer} # orders -> customers
# ... dimensions and metrics
- name: customers
semantic_model:
enabled: true
columns:
- name: customer_id
entity: {type: primary, name: customer} # same name: the join key
- name: country_code
entity: {type: foreign, name: country_code} # customers -> countries
- name: country
dimension: {type: categorical}
- name: countries
semantic_model:
enabled: true
columns:
- name: country_code
entity: {type: primary, name: country_code}
- name: region
dimension: {type: categorical}
One hop and two hops:
mf query --metrics revenue --group-by customer__country
mf query --metrics revenue --group-by customer__country_code__region
customer__country_code__region revenue
-------------------------------- ---------
Southern Europe 1240
Western Europe 990
How the name tells the path
A dimension from another model is addressed through the entities MetricFlow walks to reach it, joined by
double underscores. From orders, customer__country means “follow customer, take country”, and
customer__country_code__region follows customer and then country_code. MetricFlow allows up to two
hops.
The path starts from where the metric lives. For signups, defined on customers, the region is one hop
away and is called country_code__region. Asking for customer__country_code__region fails, and the
error suggests the right name:
The given input does not match any of the available group-by-items for
SimpleMetric('signups'). ...
Suggestions:
[
'customer__country_code',
'country_code__region',
...
The generated SQL
--explain (or --compile with dbt sl) shows the join. The two-hop query builds the customer-to-region
lookup in a subquery and left-joins it to orders:
SELECT
subq_10.country_code__region AS customer__country_code__region
, SUM(orders_src_10000.amount) AS revenue
FROM "shop"."main"."orders" orders_src_10000
LEFT OUTER JOIN (
SELECT
customers_src_10000.customer_id AS customer
, countries_src_10000.region AS country_code__region
FROM "shop"."main"."customers" customers_src_10000
LEFT OUTER JOIN
"shop"."main"."countries" countries_src_10000
ON
customers_src_10000.country_code = countries_src_10000.country_code
) subq_10
ON
orders_src_10000.customer_id = subq_10.customer
GROUP BY
subq_10.country_code__region
Left joins keep every order, even one whose customer is missing from customers; those orders show up
with an empty region instead of disappearing from the total.
Joins that are not allowed
MetricFlow only joins towards a side with at most one row per key: foreign or primary to primary or unique. A metric on customers grouped by an order dimension would need the opposite, and is refused:
mf query --metrics signups --group-by order__status
ERROR: Got error(s) during query resolution.
The given input does not match any of the available group-by-items for
SimpleMetric('signups'). Common issues are:
* Incorrect names.
* No valid join paths exist from the measure to the group-by-item.
(fan-out join support is pending).
That protects the number: each customer has several orders, and the join would count the same signup
several times. If you need “signups by the status of their first order”, build that attribute on the
customers model (for example first_order_status) in dbt, and expose it as a dimension there.
Metrics from two models in one query
mf query --metrics revenue,signups --group-by customer__country
customer__country revenue signups
------------------- --------- ---------
DE 990 2
FR 1
IT 1240 3
Each metric is computed on its own model, then the results are combined with a full outer join on the group-by columns. France has a signup but no revenue, and still appears.
Tested with dbt Core 1.12 and MetricFlow 0.213 on DuckDB.