The event
A customer buys coffee
The checkout captures one observable business event: a product was sold.
An example-driven field guide
Learn dimensional modeling by growing one tiny retail sale into a model that can answer historical, operational, and enterprise questions—without losing sight of what one row means.
The durable idea: capture measurements at a declared business grain, surround them with descriptive context, and reuse that context consistently across business processes.
Chapter 1 · Mental model
A dimensional model is a deliberate presentation model. It makes business events easy to count, sum, filter, and compare.
The event
The checkout captures one observable business event: a product was sold.
The measurement
Quantity, sales amount, discount, and cost are numeric facts produced by that event.
The context
Date, product, store, customer, and promotion dimensions describe the event.
Quick test: if a value is used to group or filter, it is probably dimension context. If it is produced by the event and meaningfully aggregated, it is probably a fact. Numeric identifiers such as product codes are still dimensions, not measurements.
Chapter 2 · Foundations
Do not pick columns first. The grain decides which dimensions and facts are valid.
Model an observable business activity, not a department or a subject noun.
Checkout saleWrite the exact meaning of one fact-table row in plain language.
One product scan on one completed receiptAsk which descriptions are true for exactly one row at that grain.
Date · Product · Store · Customer · PromotionAdd measurements produced by the process and valid at the declared grain.
Quantity · Sales · Discount · CostBalance what people need to decide with what source systems actually capture.
Example: users want walk-in conversion, but no footfall is recorded; that fact cannot be invented.
Business representatives, source experts, and DW designers make the four decisions together.
Example: finance defines “net sales” while POS engineers confirm discount fields and timing.
Add compatible dimensions, attributes, facts, or lower-grain rows without invalidating old queries.
Example: add cashier context to each receipt line; existing product-by-store analysis still works.
“Sales” is vague. Ordering, shipping, invoicing, payment, and return are distinct measurement processes.
Example: order date belongs to ordering; delivery date belongs to fulfilment.
Chapter 3 · Grain
The grain is a binding contract. Prefer the most atomic event the business captures, then keep every key and measurement faithful to it.
One row per product scan on a completed receipt.
| Receipt | Line | Product | Quantity | Line sales | Why it belongs |
|---|---|---|---|---|---|
| R-9007 | 1 | Coffee beans | 2 | $24 | One product scan |
| R-9007 | 2 | Filter papers | 1 | $6 | Another product scan |
| R-9007 | — | All products | 3 | $30 | Wrong: receipt total mixed with lines |
Header trap
If the receipt-level freight charge is copied to both lines, summing returns $24.
Fix: allocate it to line grain, or keep it in a header-grain fact.Mixed-grain trap
A daily store total represents a different row meaning. Put it in a separate aggregate fact.
Rule: different grains live in different physical fact tables.Chapter 4 · The star
Foreign keys and measurements live in the fact table. Wide, flat dimension tables hold the human descriptions used to filter and group those measurements.
Safe to sum across every dimension. Net sales and quantity.
Safe across some dimensions, not time. Daily closing inventory.
Never sum directly. Margin %; divide summed margin by summed sales.
Foreign keys plus same-grain measurements; usually narrow and deep.
Each receipt line carries five dimension keys and four amounts.
Numeric facts may be null when legitimately unknown. Foreign keys should point to an explicit Unknown dimension row.
Unknown customer → customer_key 0, not a null join.
Use identical definitions and units across processes—or use different names.
“Net sales” must treat tax and discounts the same in store and web sales.
Add lower-level dimension attributes to the grouping; no special physical structure is required.
Year → month → day, or category → brand → product.
Pre-summarize atomic facts for speed using conformed rollups.
Monthly brand sales derived from receipt lines.
Combine measurements only when their grain is exactly compatible.
Monthly product actual and forecast can share a row; daily SKU sales cannot.
Chapter 5 · Fact patterns
The same business can need several fact tables because event history, regular state, and workflow progress answer different questions.
Insert-only events
Rows appear only when a sale happens. This is the atomic, flexible foundation for detailed analysis.
Question: what sold, where, and for how much?Regular state
Dense snapshots record state for each scheduled period—even when there was no activity.
Closing balance is semi-additive: do not sum it through time.One evolving workflow row
Insert at order time, then update predictable milestone dates and duration facts as fulfilment advances.
Best for stable workflows, not unpredictable repeated events.The row is the event
A fact can contain only dimension keys. Count rows to measure occurrence; subtract activity from valid coverage to find non-events.
Build only legitimate coverage combinations, not an unbounded Cartesian product.Maya · Coffee class · Monday
Noah · Coffee class · Monday
Maya · Coffee class · Monday
Missing: Noah was absent
Optional row ID helps ETL resume, backout, and identify a fact row directly.
load_row_id 8421 tracks an interrupted batch insert.
Store milestone lag from a common start; any pairwise duration is subtraction.
ship_lag=2, deliver_lag=4 → transit=2 days.
Copy header dimension keys and degenerate IDs onto a line-grain fact.
Order channel applies to every line; freight must be allocated.
Store transaction-currency and standard-currency facts plus currency context.
€18 local and A$30 standard using the approved order-date rate.
Store agreed base-unit measures and row-level conversion factors; expose audience-specific views.
6 bottles × 750 mL → 4.5 base litres.
Avoid many keys for hierarchy levels and tiny flags.
One product key replaces separate brand/category/department keys; a junk dimension absorbs flags.
Calculate with it → fact. Filter/group stable meaning → dimension attribute or band. Sometimes keep both.
package_weight is product context; shipped_weight is an event measure.
Calculate YTD from atomic facts in the query/semantic layer.
Do not persist sales_ytd, sales_qtd, sales_mtd on every row.
Rare pattern: effective/expiry dates represent long-lived, slowly changing measurements.
Account credit limit stays $5,000 from Jan–Jun without six identical snapshots.
Usually avoid generic measure_name/value rows; they inflate data and hinder math.
Use only when hundreds of possible measures are extremely sparse.
Chapter 6 · Dimension toolkit
Dimensions trade storage efficiency for usability. They are deliberately wide, descriptive, and easy to browse.
Identity
A warehouse-controlled integer identifies one dimension row version. The source ID remains an attribute.
C-42 · Sydney · effective until 9 Aug
C-42 · Melbourne · effective from 10 Aug
Stable identity shared by both history rows
Calendar
A reusable date dimension carries fiscal periods, holidays, weekdays, and reporting labels.
| date_key | day | month | fiscal | holiday? |
|---|---|---|---|---|
| 20260818 | Tuesday | Aug 2026 | FY27 P02 | No |
One physical Date dimension appears as Order Date, Ship Date, and Delivery Date through separate keys/views.
A transaction identifier with no descriptive attributes stays in the fact—without a one-column dimension table.
Bundle small flags into one dimension; create observed combinations instead of every possible combination.
Natural keys come from sources and can change; durable keys identify the same entity across renumbering and row versions.
Employee 742 and rehired employee 991 share durable person P-18.
Put hierarchy labels on each lowest-level row for simpler queries.
Product row repeats brand, category, and department.
Meaningful labels belong in dimensions, not unexplained 0/1 fields in facts.
promotion_status = “Active”, not promo_flag = 1.
Use explicit business labels such as Unknown, Not Applicable, or Not Yet Available.
A missing suburb groups under “Unknown suburb” instead of disappearing.
One dimension may carry alternate valid rollups.
Day → Month → Year and Day → Week → Fiscal Period.
Use positional columns when level names and depth are stable.
Product → Brand → Category → Department.
Force a small known depth range into fixed positions using consistent fill rules.
Australia → NSW → Sydney versus Singapore → [country/city].
Use an ancestor–descendant bridge for arbitrary or shared paths.
An employee may be six levels deep and report through two structures.
Normalized dimension tables are accurate but harder to browse and usually discouraged in the presentation layer.
Prefer Product with brand/category columns over chained product→brand→category tables.
A dimension-to-dimension reference is a selective exception.
A customer row may reference an account-manager dimension when that reusable context is genuinely separate.
Chapter 7 · Slowly changing dimensions
Maya moves from Sydney to Melbourne on 10 August. The right technique depends on the question historical sales should answer.
Replace Sydney with Melbourne on the same row. All old sales now report as Melbourne.
Close the Sydney version and insert a Melbourne version. Facts retain the key effective at event time.
Store current_city and previous_city on one row. Useful for a limited current-versus-prior comparison.
Type 2 query lens
As-was joins each fact to its historical surrogate row. Sales retain the customer city effective when they happened.
Never change the attribute.
Original signup channel remains “Store” forever.
Split rapidly changing profile attributes; the fact carries base customer and profile keys.
Age band, loyalty tier, and risk band form a small Profile dimension.
Type 4 plus a Type-1 current-profile reference on the base dimension.
Facts answer tier “as was”; Customer answers current tier without scanning facts.
Type-2 rows also carry overwritten current attributes.
Every Maya version retains historical_city and is updated with current_city=Melbourne.
Facts store version surrogate and durable entity keys; views expose historical or current joins.
customer_key gives “as was”; durable_customer_key gives “as is”.
Use effective/expiry timestamps and a current flag; allow one current, non-overlapping row per durable identity.
Never update old fact keys to the newest customer row.
Chapter 8 · Complex relationships
A classic star expects one member of each dimension per fact row. Use bridges or allocations only when the business relationship genuinely breaks that rule.
Many-to-many example
T1 · diagnosis_group G10
cost $900
G10 → Asthma · 60%
G10 → Allergy · 40%
Asthma
Allergy
Double-counting guard: joining through a bridge repeats the fact. Use approved weights when apportioning $900, and verify weights sum to 1. Sometimes full event credit is valid for each diagnosis—but the cost still must not be summed twice.
Different-grain example
The driver—weight, value, volume, quantity—must be business-approved and auditable. Copying $12 to both lines would double it.
Correlating two Type-2 dimensions through an outrigger can explode versions; a periodic fact can observe their relationship instead.
Monthly customer × account-manager assignment.
Persist periodic text classifications when users compare positions over time.
New → Active → Active → Lapsed customer tags.
Persist an expensive analysis as a durable-key set; combine sets by union, intersection, or difference.
“Bought espresso machine, not beans within 30 days.”
Copy selected lifetime metrics or bands onto a dimension for fast filtering.
Customer lifetime_spend_band = “$1k–$5k”.
Range-join a small band dimension at query time when thresholds change.
Basket $74 joins to “$50–$99” without rewriting facts.
Free text belongs in a comments dimension referenced by a fact, not in numeric fact columns.
Return reason note “seal broken on arrival”.
Describe a sequential event’s current step and remaining steps.
Fulfilment step 3 of 5 = Packed; 2 steps remain.
Pair one fact with alternative dimension copies when each audience has a private classification.
Investors classify the same stocks using different sector taxonomies.
Avoid one vague Person or Location dimension for unrelated business roles.
Customer and employee have different attributes and should stay legible.
Chapter 9 · Enterprise integration
Build incrementally by process. Conformed dimensions let separate fact tables align without forcing them into one giant model.
The bus matrix
| Business process | Date | Product | Customer | Store | Supplier |
|---|---|---|---|---|---|
| Sales receipt line |
✓ | ✓ | ✓ | ✓ | |
| Returns returned line |
✓ | ✓ | ✓ | ✓ | |
| Inventory product/store/day |
✓ | ✓ | ✓ | ||
| Purchasing PO line |
✓ | ✓ | ✓ |
Read across to design one process. Read down to see which dimensions need enterprise agreement. A check mark promises shared definitions, values, and governance—not merely a shared column name.
Drilling across
Group sales by conformed Month + Brand.
Group returns independently by the same labels.
Align identical row headers; calculate return rate.
Never join atomic fact tables directly. Multiple sales × multiple returns for the same product creates fan-out and inflated amounts. Aggregate separately, then sort/merge on conformed labels.
Attributes and domain values mean the same thing across fact tables.
Product Brand is identical in Sales, Returns, Inventory, and Purchasing.
A row/column subset inherits vocabulary from a base conformed dimension.
Month rolls up Date; Brand rolls up Product for monthly forecasts.
A measure has identical business meaning and units across processes.
If web “revenue” includes tax but store “revenue” does not, rename rather than falsely conform.
Map the natural sequence of measurement processes.
Purchase → Receive → Stock → Sell → Return.
Deliver manageable process increments integrated by conformed dimensions; it is not tied to a platform.
Ship Sales first, then Returns reusing Product, Customer, Store, and Date.
Cross candidate initiatives with interested groups to prioritize delivery.
Returns analytics helps Store Ops, Finance, and Merchandising—high shared value.
Technical implementation of drilling across: independent aggregates, then merge.
Sales pass + inventory pass joined only after both reach Month × Brand.
Chapter 10 · Operations + edge cases
Feeds arrive late, definitions change, loads fail, and some products refuse a single schema. These techniques keep the presentation model honest.
Late fact
A 5 Aug sale arrives on 15 Aug. Maya moved on 10 Aug, so look up the customer row effective on 5 Aug: Sydney key 101.
Late dimension
A sale arrives for C-77 before customer details. Create one placeholder with Unknown labels, attach the fact, then Type-1 fill it later.
Tag fact rows with load metadata and quality outcomes.
run 882 · code v3.4 · source POS-A · currency rule FX-17 · passed 6 checks.
Carry universal and local role-playing date/time keys.
Checkout: 23:30 UTC on 17 Aug = 09:30 Sydney on 18 Aug.
Keep a small hot partition minimally indexed; blend it with stable historical partitions.
Today’s orders refresh every minute; closed days remain optimized.
Use an ETL-only fact at one detected error plus detail rows for implicated columns.
Error 88: invalid currency on order 1001; details identify source field and rejected value.
Keep common measurements in a core schema and specialized facts in product-specific schemas.
All accounts share balance; Mortgage adds LTV/term, Checking adds overdraft/fee plan.
Revenue minus allocated component costs enables atomic profit, but allocation governance is substantial.
Product profit includes weight-allocated freight and activity-allocated warehouse cost.
Move expensive, shared classifications into governed ETL only when query-time derivation is too costly.
Nightly lifecycle tag; quarterly lifetime-value band.
Insert the correctly dated version and restate affected fact foreign keys.
A city change effective 1 Aug is received on 20 Aug; re-key facts from the missing interval.
Chapter 11 · Apply it
Walk these questions in order. If an early answer is fuzzy, later schema details are premature.
Name a verb: order, ship, receive, pay, return—not a team, report, or entity.
Declare every atomic qualifier. Test three sample rows against the sentence.
If not, revisit grain or deliberately design a bridge.
Allocate header facts, or move different-grain measures to another table.
Label additive, semi-additive, and non-additive behavior; store additive components.
Choose Type 0, 1, 2, 3, or an advanced hybrid per attribute—not per whole dimension.
Use the bus matrix to expose shared vocabulary across process fact tables.
Define Unknown rows, placeholders, event-time lookups, audit context, and reconciliation.
Final test: give the model to an analyst with one business question. Can they find the right fact, understand one row, choose descriptive labels, and aggregate without hidden rules? If yes, the design is doing its job.
Chapter 12 · Continue learning
This lesson paraphrases the Kimball Group’s official technique pages. The examples and teaching sequence are original to this page.
Kimball Group is the source of the modeling techniques and terminology. No text on this page is intended as a substitute for the canonical descriptions or The Data Warehouse Toolkit.