Hierarchies and Bridge Tables in Dimensional Modeling

Modeling hierarchies and many-to-many relationships in a warehouse: fixed and ragged hierarchies, flattening, bridge tables, weighting factors and double counting.

Advanced 4 min read · Updated

A star schema assumes each dimension has one value per fact row and that hierarchies have a fixed number of levels. Real data breaks both assumptions: a bank account has several owners, a patient visit has several diagnoses, an org chart is eleven levels deep in one department and three in another. This page covers how dimensional models handle hierarchies of every shape, and how bridge tables handle many-to-many relationships without double counting.

Fixed-depth hierarchies: flatten them

When every member has the same levels, as with product → subcategory → category → department or day → month → quarter → year, store each level as its own column in one dimension row. This is the default and the easiest to query: drilling down is just adding a column to the group by.

product_keyproduct_namesubcategorycategorydepartment
501Trail Runner 2Running shoesFootwearSports
502Yoga mat ProYogaFitness equipmentSports

A dimension can hold several hierarchies side by side (calendar and fiscal, or merchandising and supplier), each as its own set of columns.

Slightly ragged hierarchies: fill the gaps

Geography is the classic case: most addresses have city, state and country, but some countries have no states. When the variation is small and the maximum depth is known, keep the fixed columns and fill the missing level with a sensible value (repeat the city, or use "No state"). Queries stay simple and totals stay correct.

Ragged, variable-depth hierarchies

Org charts, cost-centre trees, bills of materials and nested category trees have no fixed depth. A recursive parent_id column describes them but makes queries like "all sales by everyone under this VP" painful: a recursive CTE on every report. The dimensional answer is a hierarchy bridge table (sometimes called a closure table) with one row for every ancestor-descendant pair, including each node paired with itself.

Dev (Engineer)Chen (Manager)Ben (Director)Farah (Engineer)Esi (Manager)Asha (VP)
A ragged org hierarchy. The bridge table stores every ancestor-to-descendant path, so "everything under Asha" is one join with no recursion.
ancestor_keydescendant_keydepth_from_ancestoris_leaf
AshaAsha0No
AshaBen1No
AshaChen2No
AshaDev3Yes
AshaEsi1No
AshaFarah2Yes
BenChen1No
…………
-- Sales credited to everyone in Asha's organisation, at any depth
select sum(f.amount)
from fact_sales f
join bridge_org b    on f.employee_key = b.descendant_key
join dim_employee a  on b.ancestor_key = a.employee_key
where a.employee_name = 'Asha';

The bridge is maintained by the pipeline whenever the tree changes. Add valid_from and valid_to columns if reorganisations must be reported historically, so "Asha's org as of last March" is a filter on the bridge.

Multivalued dimensions and bridge tables

Sometimes the problem is not depth but multiplicity: one fact row relates to several members of a dimension. A joint bank account has two owners. A hospital visit has three diagnoses. A shared ride carries four riders. A movie has several genres. Putting one of them on the fact loses the others; repeating the fact row once per member double counts the measure.

The solution is a group: give each distinct combination of members a group key, store that key on the fact, and list the members of each group in a bridge table.

group_keycustomer_keyfact_account_balanceaccount_owner_group_key · balancebridge_owner_groupgroup_key · customer_key · weightdim_customername · segment · city
A multivalued dimension. The fact points at a group; the bridge lists the group's members with a weighting factor that sums to 1 per group.
owner_group_keycustomer_keyweighting_factor
900Priya0.5
900Rahul0.5
901Meera1.0

Weighting factors and double counting

Joining through a bridge multiplies each fact row by the number of members in its group. That is exactly right for some questions and badly wrong for others, so decide which one the user is asking:

  • Impact report (unweighted): "total balances of accounts that Priya owns, in whole or part". Each member sees the full amount; the grand total across all customers is larger than the bank's real total. Correct for per-customer views, wrong for totals.
  • Weighted report: multiply the measure by the weighting factor. Each owner gets their share, and the grand total equals the true total. Use this whenever a number will be added up across members.
  • Primary member only: some models also store a single "primary" member directly on the fact for simple reports that only need one.
-- Weighted: totals add up to the real total
select c.segment, sum(f.balance * b.weighting_factor) as balance
from fact_account_balance f
join bridge_owner_group b on f.account_owner_group_key = b.owner_group_key
join dim_customer c       on b.customer_key = c.customer_key
group by c.segment;

The interview trap: drawing a many-to-many relationship as a direct join between the fact and the dimension. Any sum across that join double counts. Say "bridge table with a weighting factor" and explain which reports are weighted.

Alternatives to a bridge

ApproachWhen it's enough
Change the grainIf each member has its own measure (each rider's own fare on a shared ride), make the fact one row per member and the problem disappears.
Primary member on the factWhen most reports need only one member, such as the primary diagnosis or the main account holder.
Positional columnsA small, fixed maximum (genre_1, genre_2, genre_3). Simple but awkward to filter on "any genre is Comedy".
Array columnWarehouses with array types can store genres as an array and unnest at query time. Convenient, but weighting is still your job.

Where it’s used

Banking

Joint accounts and account-to-customer relationships through an owner-group bridge with weights.

Healthcare

Several diagnoses per visit through a diagnosis-group bridge, with a primary diagnosis on the fact.

HR and finance

Org charts and cost-centre trees as hierarchy bridges for roll-up reporting at any level.

In an interview

  • Flatten fixed-depth hierarchies into the dimension; use a bridge only for ragged ones.
  • For many-to-many, use a group key on the fact and a bridge with weighting factors.
  • Say which reports are weighted (totals) and which are impact reports (per member).
  • Consider changing the grain first: it often removes the many-to-many entirely.

Frequently asked questions

How do you model a many-to-many relationship in a star schema?

Store a group key on the fact table and list each group's members in a bridge table with a weighting factor that sums to one per group. Weighted reports then add up to the true total.

How do you model a hierarchy with variable depth?

With a hierarchy bridge table that stores every ancestor-descendant pair and the depth between them, so rolling up everything under a node is a single join instead of a recursive query.

Further reading