Cardinality
Understand relationship cardinality types.
Strata does not support many_to_many relationships. Use junction/bridge tables with two separate relationships (one_to_many and many_to_one) instead.
Overview
Cardinality defines the relationship type between two tables. It tells the planner how rows in one table relate to rows in another.
Cardinality Types
many_to_one
Most common type. Many rows in left table match one row in right table.
Example: Many orders belong to one customer
orders_customer:
left: Orders
right: Customer
sql: left.customer_id = right.id
cardinality: many_to_one
Use cases:
- Fact table to dimension table
- Child to parent relationships
- Foreign key relationships
one_to_many
One row in left table matches many rows in right table.
Example: One customer has many orders
customer_orders:
left: Customer
right: Orders
sql: left.id = right.customer_id
cardinality: one_to_many
Use cases:
- Parent to child relationships
- One-to-many associations
- Reverse of many_to_one
one_to_one
One row in left table matches one row in right table.
Example: One user has one profile
user_profile:
left: User
right: User Profile
sql: left.id = right.user_id
cardinality: one_to_one
Use cases:
- User profiles
- Settings tables
- Extension tables
many_to_many Not Supported
⚠️ Strata does not support many_to_many relationships.
Workaround: Create a junction/bridge table and use two relationships:
# Instead of many_to_many:
# Users <-> Roles
# Create:
# Users -> UserRoles -> Roles
users_user_roles:
left: Users
right: User Roles
sql: left.id = right.user_id
cardinality: one_to_many
user_roles_roles:
left: User Roles
right: Roles
sql: left.role_id = right.id
cardinality: many_to_one
Choosing Cardinality
Fact to Dimension (many_to_one)
sales_customer:
left: Sales # Fact table (many rows)
right: Customer # Dimension table (one row per customer)
sql: left.customer_id = right.id
cardinality: many_to_one
Dimension to Dimension (one_to_one)
customer_address:
left: Customer # One customer
right: Address # One address per customer
sql: left.address_id = right.id
cardinality: one_to_one
Parent to Child (one_to_many)
customer_orders:
left: Customer # One customer
right: Orders # Many orders per customer
sql: left.id = right.customer_id
cardinality: one_to_many
Measure Expansion
By default, measures can only be aggregated along one_to_one join paths. The allow_measure_expansion property enables measures to aggregate along many_to_one or one_to_many paths, but use with caution to avoid double-counting.
The Double-Counting Problem
When you join a fact table to a dimension through a many_to_one relationship and then to another table, measures might be counted multiple times:
Orders (fact) → Customer → Customer Address
100 rows 50 rows 50 rows
If you query Total Revenue grouped by Address City, each order amount could be counted once (correct) or multiple times (incorrect) depending on the join path.
Using allow_measure_expansion
Set allow_measure_expansion: true when you're certain the join path won't cause double-counting:
# Safe: Customer has exactly one address
customer_address:
left: Customer
right: Customer Address
sql: left.address_id = right.id
cardinality: many_to_one
allow_measure_expansion: true # Measures can flow through this join
When to Use
| Scenario | allow_measure_expansion | Why |
|---|---|---|
| Fact → Dimension (standard) | Not needed | Default behavior handles this |
| Dimension → Dimension (1:1 logical) | true | Safe when truly 1:1 in practice |
| Dimension → Lookup table | true | Safe for simple lookups |
| Any path that could fan out | false (default) | Prevents double-counting |
Example: Safe Expansion
datasource: warehouse
# Orders to Customer: standard fact-to-dimension
orders_customer:
left: Orders
right: Customer
sql: left.customer_id = right.id
cardinality: many_to_one
# Customer to Address: each customer has one address
# Safe to expand measures through this join
customer_address:
left: Customer
right: Customer Address
sql: left.address_id = right.ca_address_sk
cardinality: many_to_one
allow_measure_expansion: true
With this configuration, you can query Total Revenue by Address City and get correct results.
Automatic Behavior
one_to_onerelationships: Measures always expand (bidirectional)many_to_onewithout flag: Only dimensions expand, measures stopmany_to_onewithallow_measure_expansion: true: Measures expand through
Internal Behavior Notes
- one_to_many is automatically converted to many_to_one internally (direction flipped)
- one_to_one creates bidirectional joins automatically — you can traverse either direction
- Tables in a relationship must be in the same datasource
Best Practices
- Use many_to_one for fact-to-dimension relationships
- Match database relationships - Cardinality should reflect actual data
- Avoid many_to_many - Use junction tables instead
- Use allow_measure_expansion sparingly - Only when you're certain it won't cause double-counting
- Document relationships - Add comments explaining why expansion is safe