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:

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.

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.

References