Chapter 09 · Bridge Tables and Many-to-Many Analytical Relationships
Validate Many-to-Many Queries with Control Totals to Prove No Double Counting
Build an executable bridge acceptance suite covering row multiplication, weight normalization, missing membership, temporal correctness, allocation reconciliation, and safe rollups.
Learning outcomes
A bridge is safe only when its invariants are executable. AtlasMart therefore tests the base fact grain, joined cardinality, normalized weights, historical allocation distribution, grand-total reconciliation, temporal coverage, overlap absence, and negative cases. This acceptance suite is designed to fail loudly when a bridge load or BI query silently changes semantics.
Assemble bridge correctness into a deterministic local test suite.
Assert both atomic control totals and allocated rollup totals.
Inject missing membership and bad weights to prove tests detect failures.
Validate temporal distribution, not merely the grand total.
Publish production acceptance criteria covering semantics, lineage, observability, and rollback.
Chapter 09 starts from the accepted Chapter 08 state. Canonical paid sales remain seven order-line facts, four paid orders, nine units, 625 paid GMV, 380 cost-at-sale, and 245 gross profit. Customer history keeps Chapter 07's half-open business-effective intervals; Chapter 08's date-role, junk, degenerate, mini-dimension, and inferred-member patterns remain unchanged. Chapter 09 adds a synthetic customer-to-household many-to-many relationship solely to teach bridge semantics. The bridge does not alter the grain of fact_sales_history, does not rewrite customer surrogate keys, and does not redefine the accepted sales measures.
The mandatory lab uses Python's standard-library
sqlite3 module and synthetic local data. Record
the actual Python and SQLite versions before execution.
Allocation weights, household membership, household types, and
effective dates are explicit AtlasMart teaching contracts, not
universal business rules. The local fixture proves relational
and arithmetic semantics in one process; it does not prove
distributed concurrency, cloud optimizer behavior, CDC
ordering, production privacy policy, or performance at
warehouse scale.
No single invariant is enough. A bridge can reconcile while using the wrong history, have valid history while missing facts, or have complete coverage while using weights that do not follow the approved business rule.
1. Acceptance matrix
| Invariant | Expected | Failure means |
|---|---|---|
| base fact controls | 7 rows; 4 orders; 9 units; 625 GMV; 380 cost; 245 profit | upstream fact drift or wrong grain |
| temporal join cardinality | 11 rows | relationship population changed; investigate before comparing money |
| unweighted joined GMV | 900 in deliberate counterexample | confirms why raw sum after bridge is unsafe |
| weight normalization | zero violating intervals | allocation contract broken |
| temporal coverage | zero missing allocatable fact rows | bridge missing relationship |
| same-member overlap | zero rows | ambiguous temporal membership version |
| weighted grand total | 625 | allocation loses or multiplies money |
| weighted household distribution | 50/225/200/75/75 | temporal/business rule drift |
| SUM(DISTINCT amount) | 375 in counterexample | proves DISTINCT is not a grain repair |
2. Executable acceptance script
import sqlite3# build_conn() seeds the deterministic Chapter 09 fixture.conn = build_conn()assert base_controls(conn) == (7,4,9,625,380,245)joined = conn.execute("""SELECT COUNT(*),SUM(f.extended_amount)FROM fact_sales_history fJOIN bridge_customer_household b ON b.durable_customer_id=f.durable_customer_id AND b.effective_from <= f.event_date AND f.event_date < b.effective_to""").fetchone()assert joined == (11,900), joinedassert conn.execute("SELECT SUM(DISTINCT extended_amount) FROM fact_sales_history").fetchone()[0] == 375assert weight_interval_audit(conn) == []assert overlap_audit(conn) == []assert allocation_rows(conn) == EXPECTED_ALLOCassert round(sum(v for _,v in allocation_rows(conn)),2) == 625.00missing = conn.execute("""SELECT COUNT(*)FROM fact_sales_history fLEFT JOIN bridge_customer_household b ON b.durable_customer_id=f.durable_customer_id AND b.effective_from <= f.event_date AND f.event_date < b.effective_toWHERE b.household_sk IS NULL""").fetchone()[0]assert missing == 0# Negative test 1: remove Ben's membership; coverage must fail and expose 200 GMV.bad = build_conn()bad.execute("DELETE FROM bridge_customer_household WHERE durable_customer_id='D-CUST-002'")miss = bad.execute("""SELECT COUNT(*),SUM(f.extended_amount)FROM fact_sales_history fLEFT JOIN bridge_customer_household b ON b.durable_customer_id=f.durable_customer_id AND b.effective_from <= f.event_date AND f.event_date < b.effective_toWHERE b.household_sk IS NULL""").fetchone()assert miss == (1,200), miss# Negative test 2: corrupt a normalized weight set.bad2 = build_conn()bad2.execute("""UPDATE bridge_customer_householdSET allocation_weight=0.50WHERE durable_customer_id='D-CUST-001' AND household_sk=1 AND effective_from='2026-01-01'""")assert weight_interval_audit(bad2) != []# Negative test 3: current membership can preserve 625 but distort history.current = conn.execute("""WITH current_bridge AS ( SELECT * FROM bridge_customer_household WHERE effective_from <= '2026-09-20' AND '2026-09-20' < effective_to)SELECT h.household_code,ROUND(SUM(f.extended_amount*b.allocation_weight),2)FROM fact_sales_history fJOIN current_bridge b ON b.durable_customer_id=f.durable_customer_idJOIN dim_household h ON h.household_sk=b.household_skGROUP BY h.household_code ORDER BY h.household_code""").fetchall()assert round(sum(v for _,v in current),2) == 625.00assert current != EXPECTED_ALLOCprint('base_controls=', base_controls(conn))print('joined_counterexample=', joined)print('historical_allocation=', allocation_rows(conn))print('Chapter 09 acceptance: PASS')
Expected final line: Chapter 09 acceptance: PASS.
The negative cases matter as much as the happy path: the suite
proves it can detect missing coverage and broken normalization,
and it demonstrates a subtle case where a wrong
current-membership replay still conserves the grand total.
3. Production observability and lineage
Monitor relationship-row counts, weight-normalization failures, missing-membership amount, bridge overlap violations, allocated-vs-atomic reconciliation delta, and the age/version of membership data. Lineage should connect the source membership decision to bridge rows, allocation definition, transformed metric, and consuming report. A 0.00 reconciliation delta without bridge version metadata is not enough for audit.
4. Performance without sacrificing correctness
Bridge joins can expand rows. Index the relationship lookup columns appropriate to the engine and measure representative workloads, but never “optimize” by removing the temporal predicate or weight multiplication unless the business question changes. Pre-aggregated or semantic-layer views can simplify BI, provided their grain, freshness, and reconciliation contracts remain explicit.
This SQLite lab is intentionally tiny and in-memory; it provides no warehouse-scale latency or optimizer claim. Any production benchmark must disclose engine, data size, relationship cardinality distribution, indexes/partitioning, cache state, query corpus, and concurrency.
5. Change and rollback contract
A bridge policy change can restate historical allocated metrics without changing any atomic fact. Treat that as a semantic migration: version the rule, identify affected intervals/facts, calculate before/after controls, validate downstream reports, communicate ownership, and retain a rollback path. If the new bridge load fails coverage or normalization, keep the prior published version rather than partially exposing mixed semantics.
6. Cleanup/reset
The mandatory fixture is in-memory SQLite; closing the process
discards it, and build_conn() restores the
deterministic accepted state. No cloud account, production data,
credentials, or destructive external cleanup is required.
Knowledge check
Check your understanding
- Why does the acceptance suite assert joined row count as well as money totals?
- What negative case proves missing bridge coverage is visible?
- Why is the current-membership negative test especially important?
- Which bridge metrics should production observability expose?
- What should happen when an allocation policy changes?
Review the answers
1. Cardinality is direct evidence of relationship expansion and helps diagnose multiplication before aggregate values hide it.
2. Deleting D-CUST-002 membership produces one unresolved fact row carrying 200 GMV.
3. It shows the grand total can still reconcile while historical household attribution is wrong.
4. Coverage/unallocated amount, weight normalization, overlap violations, relationship cardinality, reconciliation delta, freshness/version, and lineage.
5. Treat it as a versioned semantic migration with impact analysis, restatement policy, reconciliation, downstream validation, communication, and rollback.
Summary and next step
Chapter 09 now has a measurable bridge contract: relationship expansion is visible, allocation is governed, temporal history is reconstructed as-of event time, and control totals prove conservation without confusing conservation with semantic correctness. Chapter 10 next separates transaction, periodic snapshot, and accumulating snapshot fact designs.
Authoritative references
- Kimball Group — Multivalued Dimensions and Bridge Tables — Primary reference for legitimate multivalued dimensional relationships, group keys, and effective dating when membership changes over time.
- Kimball Group — Design Tip #142: Building Bridges — Implementation-oriented discussion of bridge patterns for multivalued dimensions.
- Kimball Group — Design Tip #166: Potential Bridge Table Detours — Explains bridge usability tradeoffs and the over-counting risk when measures cross a multivalued relationship without allocation.
- Kimball Group — Ragged/Variable Depth Hierarchies — Reference for hierarchy bridge/closure-style modeling when fixed-depth attributes are insufficient.
- Kimball Group — The 10 Essential Rules of Dimensional Modeling — Reinforces keeping the fact grain intact and resolving legitimate multivalued relationships with a bridge instead of corrupting the measurement event.
- SQLite — CREATE TABLE — Constraint semantics used by the local deterministic bridge lab.
- SQLite — Foreign Key Support — Local referential-integrity behavior used by the bridge/member fixture.