Lesson 7 of 8 · 50 min

Warehouse facts, dims & events

Star schema, additive vs semi-additive measures, SCD2 point-in-time, funnels, conformed dims, partitioning and incremental loads.

Lesson 7 · Warehouse modeling

Facts, dims, SCD, events - analytics modeling that interviews probe

OLTP instincts fail in the warehouse

Analytics interviews (data eng, analytics eng, DS with modeling) expect star/snowflake literacy, fact grain, slowly changing dimensions, partition strategies, and event-stream modeling. You will not design Kimball dogma for its own sake - you will defend a grain that makes self-serve metrics safe.
Warehouse modeling optimizes for analytical questions over many rows: slice revenue by time, product, region; funnel events; cohort retention. Writes are batch/stream loads; reads are wide scans. Normalize less; dimensionalize more.

Facts and dimensions (star schema)

Fact tables store measurements at a declared grain (one row per order line, per daily snapshot, per event). Dimensions store descriptive attributes (customer, product, date). Star: facts in center, dims around. Snowflake: dims normalized further - sometimes useful, often slower for BI.
sql
1-- Fact: order_items grain2fact_order_items (3  order_item_id,4  order_id,5  date_key,           -- FK date dim6  customer_key,       -- FK customer dim (surrogate)7  product_key,8  qty,9  gross_cents,10  discount_cents,11  net_cents12)1314dim_customer (customer_key SK, customer_id NK, country, segment, ...)15dim_product  (product_key SK, sku NK, category, ...)16dim_date     (date_key, date, week, month, fiscal_year, is_holiday)1718-- Grain sentence: one row per order line per order.

Additive, semi-additive, non-additive measures

Additive: sum across all dims (line net_cents). Semi-additive: sum across some dims not time (account balance - sum customers OK, sum days not). Non-additive: ratios (conversion rate) - compute from additive components. Interviewers love balance snapshots.
sql
1-- Semi-additive: daily account balance snapshot2fact_account_daily_balance (3  date_key, account_key, balance_cents4)5-- Correct: SUM(balance) for a day across accounts6-- Wrong: SUM(balance) across days for 'total money'78-- Use last snapshot in range or average balance rules as product defines

Slowly changing dimensions (SCD)

Type 1: overwrite attribute (lose history). Type 2: new row with effective dating (valid_from/valid_to or is_current) - keep history. Type 3: limited previous-value columns. Most interview answers: Type 2 for segment/country changes that must not rewrite past facts.
sql
1dim_customer_scd2 (2  customer_key   BIGSERIAL PK,   -- surrogate per version3  customer_id    BIGINT NOT NULL, -- natural key4  country        TEXT,5  segment        TEXT,6  valid_from     TIMESTAMPTZ NOT NULL,7  valid_to       TIMESTAMPTZ,    -- null or '9999' if current8  is_current     BOOLEAN NOT NULL9);1011-- Fact points at customer_key version true at event time12-- Point-in-time join:13SELECT f.*, d.segment14FROM fact_order_items f15JOIN dim_customer_scd2 d16  ON d.customer_id = f.customer_id17 AND f.ordered_at >= d.valid_from18 AND (d.valid_to IS NULL OR f.ordered_at < d.valid_to);

Event modeling and funnels

Product analytics often uses an immutable event stream: user_id, event_name, ts, properties. Funnels count users progressing through ordered events within a window. Session tables may be derived. Keep raw events; build marts for common grains.
sql
1-- Simplified funnel: view → add_cart → purchase within 7 days2WITH views AS (3  SELECT user_id, MIN(ts) AS view_ts4  FROM events WHERE event_name = 'view_product'5  GROUP BY 16),7carts AS (8  SELECT e.user_id, MIN(e.ts) AS cart_ts9  FROM events e10  JOIN views v ON v.user_id = e.user_id11  WHERE e.event_name = 'add_cart'12    AND e.ts >= v.view_ts13    AND e.ts < v.view_ts + INTERVAL '7 days'14  GROUP BY 115),16purchases AS (17  SELECT e.user_id, MIN(e.ts) AS purchase_ts18  FROM events e19  JOIN carts c ON c.user_id = e.user_id20  WHERE e.event_name = 'purchase'21    AND e.ts >= c.cart_ts22    AND e.ts < c.cart_ts + INTERVAL '7 days'23  GROUP BY 124)25SELECT26  (SELECT COUNT(*) FROM views) AS n_view,27  (SELECT COUNT(*) FROM carts) AS n_cart,28  (SELECT COUNT(*) FROM purchases) AS n_purchase;

Wide facts vs multiple facts; conformed dims

One giant fact for everything becomes sparse and confusing. Prefer multiple facts with conformed dimensions (shared dim_date, dim_customer) so metrics combine in BI. Header vs line facts: do not mix grains in one table without a clear rule.

Partitioning, clustering, incremental loads

Partition large facts by date for prune-on-query. Cluster/sort by common filters (tenant_id, user_id) per warehouse features. Incremental models process new partitions only (dbt incremental, MERGE). Mention late-arriving data and idempotent loads - senior data eng signal.
sql
1-- Idempotent daily load sketch (conceptual)2DELETE FROM fact_order_items WHERE date_key = :load_date;3INSERT INTO fact_order_items4SELECT ... FROM staging_orders WHERE order_date = :load_date;56-- Or MERGE on natural keys for slowly changing facts7MERGE INTO fact_x t8USING staging_x s ON t.nk = s.nk9WHEN MATCHED THEN UPDATE SET ...10WHEN NOT MATCHED THEN INSERT ...;

Interview answers - warehouse

  1. 01"What is fact grain?" → one sentence: one row per X; all measures must make sense at X.
  2. 02"Star vs snowflake?" → star denormalized dims for BI speed; snowflake normalizes dims further.
  3. 03"SCD2 when?" → need attribute history for point-in-time analytics (segment, plan, country).
  4. 04"is_current join risk?" → rewrites history; use version key or valid_from/to.
  5. 05"Semi-additive example?" → balances, headcount snapshots - do not sum across time blindly.
  6. 06"Conformed dimension?" → shared dim definition across facts (date, customer).
  7. 07"Why separate event raw + marts?" → raw immutable; marts serve stable metrics with tests.
  8. 08"Late arriving fact?" → reload partition / merge; watch SCD timing.
  9. 09"Allocation problem?" → header fee to lines needs explicit rule (equal, revenue weight).
  10. 10"dbt tests?" → unique grain, not_null keys, accepted values, relationship tests.
articleKimball Group - dimensional modeling techniquesKimball Groupdocsdbt dimensional modeling guidedbt Labsdocsdbt incremental modelsdbt LabsdocsBigQuery partitioning and clusteringGoogle Cloud
Interview habit: before you write a join or window, say the grain of the result set out loud (one row per what?), name the filters that apply before aggregation, and sketch a 4-row toy table. That 30-second ritual catches double-counting and null traps faster than staring at syntax.
When stuck live, reduce to two tables and three rows on the whiteboard. Prove the join, then reintroduce filters and aggregates. Interviewers credit recovery more than a first-draft perfect query that you cannot explain.
State timezone and late-arriving data assumptions once. 'UTC calendar day of paid_at; events may land +2h' prevents half the metric arguments in marketplace and consumer analytics rounds.
Prefer COUNT(DISTINCT key) or EXISTS for existence metrics; prefer SUM of a measure that already lives at the target grain. If you must join a child table, aggregate the child in a CTE first and say why.
Read the English noun in the question: users, orders, sessions, days. That noun is usually the GROUP BY or partition key. If your SELECT invents a different noun, you probably changed the grain by accident.
Null policy belongs in the spoken answer: 'missing country is excluded from geo breakouts but included in totals' is a product decision. Silent COALESCE to 'unknown' without saying so is how dashboards disagree.
For top-N, always name the tie-break (latest id, lowest sku, highest created_at). Nondeterministic winners fail both unit tests and interviewer follow-ups about flaky results.
Close every SQL answer with two edges: empty input and a duplicate natural key. If both survive your mental dry-run, you are ready for the next prompt in a multi-question screen.

Checkpoint

fact_order_items is line grain; shipping_fee lives once per order. Analyst SUM(shipping_fee) after joining fee to every line - result?

ACorrect total shipping because joins preserve feesBInflated shipping: fee repeats per line; need allocation rule or a separate order-grain shipping factCShipping becomes zero due to NULL propagation
Sign up free to answer and see why

Checkpoint

Customer segment changes from free→pro in June. You need May revenue by segment at that time. SCD approach?

AType 1 overwrite segment, join facts to current dim onlyBType 2 versioned dim (or fact stores segment/version at load) with point-in-time join for MayCDelete May facts and reload after segment change
Sign up free to answer and see why

Checkpoint

Account balances snapshotted daily. Stakeholder asks for SUM(balance) across all days in Q1 for 'total deposits'. Issue?

ANone - summing snapshots always equals depositsBBalances are semi-additive over time; sum across days is meaningless for deposits - use transactions or define avg/end balanceCFix by DISTINCT on balance values
Sign up free to answer and see why

Checkpoint

Why prefer raw events + derived marts over only curated funnel tables?

ARaw events waste storage so they should be deleted weeklyBRaw remains source of truth for new metrics; marts provide stable tested grains for self-serveCMarts are illegal without raw tables in SQL standard
Sign up free to answer and see why

Checkpoint

Conformed dimension means?

AA dimension only used by one fact table everBA consistently defined dimension (e.g. dim_date, dim_customer) reused across facts so metrics combine cleanlyCA dimension that has been Type-1 updated only
Sign up free to answer and see why

Can you declare fact grain, pick SCD2 when history matters, and call out semi-additive measures in a design interview?

New to itGetting thereConfident

Takeaways

  • Always state fact grain in one sentence.
  • Star schema: facts + dims; conform dims across facts.
  • SCD2 + point-in-time joins preserve history.
  • Semi-additive measures: do not sum across time blindly.
  • Raw events + marts; incremental idempotent loads.

Next: capstone - live SQL + modeling mock with a full rubric.

Sources

Free to read · better with Enzo

Learn it with Enzo

Save your progress, answer the checkpoints, and let Enzo quiz you on what you just read.