An entity is a key: the column that says what a row is, or which other thing it points to. MetricFlow
uses entities, and nothing else, to decide which semantic models it may join. Getting their names and
types right is what makes customer__country work for a metric defined on orders.
Copy and paste
Primary and foreign entities on columns:
models:
- name: orders
semantic_model:
enabled: true
columns:
- name: order_id
entity:
type: primary # one row per order
name: order
- name: customer_id
entity:
type: foreign # many orders per customer
name: customer
A key built from an expression (here a table with one row per account and day):
derived_semantics:
entities:
- name: account_day
type: primary
expr: "account_id || '|' || cast(balance_date as varchar)"
A table without any key column:
- name: page_views
semantic_model:
enabled: true
primary_entity: page_view # a virtual primary entity
The four types
| Type | Meaning | Nulls | Typical column |
|---|---|---|---|
primary |
one row per value, every record present | no | orders.order_id |
unique |
one row per value, but only some records | yes | customers.email |
foreign |
points to another model’s key, many rows per value | yes | orders.customer_id |
natural |
real-world key of a slowly changing (SCD type II) table | no | sales_rep_id with validity dates |
MetricFlow joins from a model to another only when the target side has at most one row per key:
| From | To primary | To unique | To foreign |
|---|---|---|---|
| foreign | ✅ | ✅ | ❌ fan-out |
| primary | ✅ | ✅ | ❌ fan-out |
| unique | ✅ | ✅ | ❌ fan-out |
So a metric on orders can use dimensions of customers (foreign → primary), but a metric on customers cannot use dimensions of orders (primary → foreign): there would be several orders per customer. The error and what to do about it are in how to join two semantic models.
The name is the join key
name defaults to the column name, but it is the name that MetricFlow matches across models, not the
column. orders.customer_id and customers.customer_id join because both entities are called customer.
A column called cust_id in another table joins too, as long as its entity is also named customer.
Names have two rules:
- One name per element in a semantic model. An entity can’t share its name with a dimension or a
metric of the same model. Naming the entity on
country_codecountry, next to acountrydimension, failed the parse:
In semantic models {'customers'}, element `country` is of type SemanticModelElementType.DIMENSION, but
it is used as types [<SemanticModelElementType.DIMENSION: 'dimension'>, <SemanticModelElementType.ENTITY:
'entity'>] across the model.
...
Parsing Error
Semantic Manifest validation failed.
- One meaning per name across the project. Use
customerfor every key that identifies a customer, and different names for different things, even if both columns are calledid.
Every semantic model needs a primary entity
The primary entity fixes the grain of the model and keeps dimension names unique. If the table has no
single key column, build one with derived_semantics and expr, as for the balances table above, or
declare a virtual one with primary_entity, which names the grain without mapping it to a column.
Entities as dimensions
You can group by an entity directly. Revenue per customer, with no extra configuration:
mf query --metrics revenue,order_count --group-by customer --order customer
customer revenue order_count
---------- --------- -------------
10 570 5
11 740 5
12 440 4
13 230 3
14 250 2
Natural keys
natural is only for slowly changing dimension tables (SCD type II), where the same key appears in several
rows with validity dates, such as a sales rep who changed region. Combined with validity-period settings on
the time dimensions, it lets MetricFlow pick the row that was valid on each date. For ordinary tables, use
primary or unique.
Tested with dbt Core 1.12 and MetricFlow 0.213 on DuckDB.