Chapter 6 — Key Takeaways (Data Modeling)
The page to have open while designing a schema. The most durable material in the book.
The four steps — the order IS the method
| # | Step | The rule |
|---|---|---|
| 1 | Select the business process | Not a report, not a department. An activity that generates measurable events. |
| 2 | Declare the grain | One sentence, business language, written down. Before anything else. |
| 3 | Identify the dimensions | Test: can you imagine grouping by it or filtering on it? |
| 4 | Identify the facts | Test: is it numeric, and does summing it across rows mean something? |
The grain determines which dimensions are available and which measures are valid. At order-line grain you can attach product; at order-header grain you cannot. At line grain
shipping_costis invalid without an allocation rule.
Grain rules: one sentence in business language · choose the finest grain the source supports (you can aggregate up, never disaggregate) · one fact table, one grain · write it at the top of the model file and test it — a declared grain with no test is a comment.
Why model at all — the three reasons, ranked
| Reason | Weight |
|---|---|
| Performance | 🟢 weakest. Modern engines join fine. Don't model for this. |
| Comprehensibility | 🟡 put the business rules in the model once so nobody has to know them again |
| Semantic stability | 🔴 strongest. A stable interface over an unstable source. |
The refactor test: source splits a table → one file changes, forty-one downstream objects untouched. Without the layer: nine days spent finding what broke.
Normalization vs. dimensional
| OLTP | OLAP | |
|---|---|---|
| Rows touched | 1–100 | 10⁵–10⁹ |
| Columns touched | most | 3–8 of 40 |
| Optimizes | write integrity | read throughput |
| Shape | normalized | denormalized |
Analytics does not need update integrity because it does not accept user writes. So denormalize deliberately: storage is cheap, joins are not. Prefer star to snowflake — the human reading the model is the constraint.
Fact tables
| Type | Shape | Use |
|---|---|---|
| Transaction | one row per event, append-only | the default |
| Periodic snapshot | one row per entity per period | levels, not events — inventory, balances |
| Accumulating snapshot | one row per entity, updated in place | pipelines with known milestones |
Additivity — where a model error becomes a wrong dashboard
| Sums across | Example | |
|---|---|---|
| Additive | everything | quantity, net_revenue_cents |
| Semi-additive | not time | on_hand_units_eod — 400 Mon + 400 Tue = 400, not 800 |
| Non-additive | nothing | unit_price_cents, margin_pct, any ratio |
⚠️ Every BI tool defaults to
SUM. Defenses: name the column so the aggregation is obvious (on_hand_units_eod) · store the additive alternative alongside (units_received,units_shipped) · set the default aggregation in the semantic layer · test it.
Fact table conventions
- Money in integer cents, every column ending
_cents. - Store the sign convention in a comment and test it (
discount_cents >= 0). - Every dimension key
NOT NULL, pointing at a reserved "not applicable" member. Null FKs make rows vanish from inner joins. - Mark non-additive measures. Someone will sum them.
- Store the metric (
net_revenue_cents) rather than making each consumer re-derive it. - Keep the degenerate key (
order_id) for traceability back to the source.
Dimensions
Wide, denormalized, small. dim_product 47K rows × 40 columns; fct_order_item 6.48M rows × 18.
Spend columns freely in dimensions, carefully in facts.
Surrogate keys, because Type 2 history makes the natural key non-unique — that is the
decisive reason. Also: source keys change, multi-source identity, marginal join speed. Costs: a
lookup on every load, a debugging tax, and you must carry the natural key alongside.
Exception: dim_date uses YYYYMMDD — dates never change their history.
| SCD | Behavior | Use |
|---|---|---|
| 0 | never changes | birth date, signup date |
| 1 | overwrite | corrections |
| 2 | new row + valid_from/valid_to/is_current |
default when history matters |
| 3 | add a "previous" column | exactly one prior value needed |
| 4 | mini-dimension | huge, fast-changing attribute sets |
| 6 | 1+2+3 | "as-was" and "as-is" both queryable |
Type 2 only where history has a named business meaning. It costs unbounded growth, harder fact loads (resolve the historical key), and an analyst decision that can be got wrong. Keeping bronze forever is what makes this recoverable.
Conformed dimensions and the bus matrix
A conformed dimension is shared identically across fact tables — that is what lets you compare units ordered vs. shipped vs. returned in one query.
| Date | Customer | Product | Warehouse | Promotion | Channel | |
|---|---|---|---|---|---|---|
| Order placement | ● | ● | ● | ● | ● | |
| Shipment | ● | ● | ● | ● | ||
| Return | ● | ● | ● | ● | ● | |
| Inventory snapshot | ● | ● | ● |
Four jobs in one table: what to build (rows = facts, columns = dimensions) · build order (most shared dimensions first) · where the integration points are · a stakeholder can read it, which prompts "why can't we break returns down by promotion?" before you build.
The seven patterns
| Pattern | What |
|---|---|
| Degenerate dimension | A business key with no attributes. Lives in the fact. Keep it for traceability. |
| Junk dimension | Unrelated low-cardinality flags, combined. 4 flags → ≤40 rows. |
| Role-playing | One dimension joined several times. Alias with views. |
| Bridge table | Genuine many-to-many. Must carry an allocation weight summing to 1.0 per fact row — and test it. |
| Factless fact | Only keys. For counting non-events, which no ordinary fact can answer. |
| Periodic snapshot | One row per entity per period. |
| Accumulating snapshot | One row per entity, updated through milestones. |
The eight mistakes, by symptom
| Symptom | Mistake |
|---|---|
Totals will not reconcile; everyone remembers a WHERE |
Mixed grain |
| The source can answer it, the warehouse cannot | Grain chosen after columns |
| Rows silently vanish from a report | Null foreign keys |
| Number wrong by roughly the row count | Summed a non-additive measure |
| Revenue doubles when a dimension is added | Fan trap |
| Numbers inflate by orders of magnitude | Chasm trap (two facts joined via one dimension) |
| Nine joins per query; analysts email you instead | Snowflaking by default |
| Every source refactor breaks the warehouse | A model that mirrors the source ← most common |
The thing nobody warns you about
Modeling forces the business to agree with itself. You cannot declare a grain without deciding
what an order is. You cannot define net_revenue_cents without resolving cancellations, partial
returns, and shipping.
When a technical argument goes in circles, check whether it is a proxy for a definitional one. Schema debates are comfortable; definitional debates require someone to concede.
If you must fabricate a number (an allocation), make the fabrication visible in the data — a weight column beside the allocated amount is worth more than a paragraph nobody reads. And write the rounding remainder rule, or reconciliation to the cent fails on a third of rows.