◐ ai-teacher

❄️ Warehouse & Modeling · Chapter 2

Dimensional modelling: facts and dimensions

Every analytics question is a number sliced by a noun. Facts hold the numbers, dimensions hold the nouns — get that split right and every dashboard is one join away.

Study this lesson free in the app →

The basics

Dimensional modelling is a way of laying out warehouse tables so that business questions become simple queries: a central table of measurements (the fact table) surrounded by tables of descriptive context (the dimensions).

Why it exists: the app database for Meghalaya Cabs is built to record rides quickly — dozens of normalised tables, each row updated in place. An analyst asking "revenue by city by week for premium drivers" would need eight joins and would hammer the production database. Dimensional modelling reshapes the same data for reading: few joins, predictable shapes, history kept.

The mental model: a sentence. "How much fare (fact) did we earn, by which driver, in which city, on which day (dimensions)?" The verbs and numbers become facts; the who/what/where/when become dimensions.

Words you must know 5

Fact
a measurable event: a ride, a payment. Numeric, many rows, append-mostly.
Dimension
descriptive context: driver, rider, city, date. Text-heavy, fewer rows.
Measure
a numeric column in a fact you aggregate: fare_inr, distance_km.
Surrogate key
a warehouse-generated integer key for a dimension row, independent of the source system's id.
Grain
what exactly one fact row represents.

Explained

Meghalaya Cabs' ops head asks: "Average fare per km for premium drivers in Shillong, weekends only, last quarter."

On the app database that is 8 joins across rides, ride_status_log, drivers, driver_tiers, vehicles, zones, cities, payments — and a date calculation nobody agrees on.

On a dimensional model it is one fact table and three dimensions, and every analyst writes it the same way.

The shop receipt

Look at a supermarket receipt. Each line is a fact: 2 × milk, ₹54 each, ₹108.

Everything else on it is context: which store, which till, which cashier, what time, which loyalty card. Nobody adds up cashier names. Everybody adds up rupees.

Now imagine the shop's head office. They keep one huge ledger of lines (numbers + codes) and small reference books: store book, product book, calendar. To answer "dairy sales in Guwahati stores on Sundays", they flip to the codes in the reference books and add up the ledger.

That ledger is the fact table. The reference books are dimensions.

Facts and their measures, precisely

A fact table row = one occurrence of a business process at a declared grain. It holds:

Three fact shapes:

Dimensions, precisely

A dimension row describes one thing the business talks about. dim_driver: name, tier, vehicle type, joining date, home city, rating band.

Meghalaya Cabs, ride process

              dim_date (role: pickup_date)
                      │
 dim_rider ──┐        │        ┌── dim_driver
             │        │        │   driver_key PK
             ▼        ▼        ▼   driver_id (natural)
        ┌──────────────────────────┐ tier, vehicle,
        │        fact_rides        │ home_city …
        │ ride_id     (degenerate) │
        │ rider_key   FK           │
        │ driver_key  FK           │
        │ pickup_date_key FK       │
        │ pickup_zone_key FK       │
        │ ride_flags_key  FK (junk)│
        │ fare_inr      additive   │
        │ distance_km   additive   │
        │ wait_seconds  additive   │
        └──────────────────────────┘
             ▲               ▲
        dim_zone        dim_ride_flags
        zone, city,     payment_mode,
        state           is_airport, is_shared

One row in the middle = one completed ride. Every arrow is an integer join.

The question becomes boring SQL

-- Avg fare per km, premium drivers, Shillong, weekends, Q3 2026
SELECT  d.vehicle_type,
        SUM(f.fare_inr) / NULLIF(SUM(f.distance_km), 0) AS fare_per_km
FROM    fact_rides f
JOIN    dim_driver d  ON d.driver_key = f.driver_key
JOIN    dim_zone   z  ON z.zone_key   = f.pickup_zone_key
JOIN    dim_date   dt ON dt.date_key  = f.pickup_date_key
WHERE   d.tier         = 'Premium'
  AND   z.city         = 'Shillong'
  AND   dt.is_weekend  = TRUE
  AND   dt.fiscal_qtr  = '2026-Q3'
GROUP BY d.vehicle_type;

Note SUM(fare) / SUM(distance) — not AVG(fare_per_km). A ₹60 two-km hop and a ₹900 thirty-km airport run do not deserve equal weight. That is the non-additive rule in practice.

At 2 billion rides

Gotchas

Words to know

Fact tableTable of measurements of a business process at a declared grain: dimension foreign keys plus numeric measures.
Dimension tableDescriptive context (who, what, where, when) used to filter and group facts; wide, denormalised, relatively few rows.
Surrogate keyA meaningless warehouse-generated integer primary key for a dimension row, separate from the source system's natural key.
Semi-additive measureA measure that can be summed across some dimensions but not across time, such as an account balance.
Conformed dimensionA dimension shared with identical keys and attributes by several fact tables, enabling cross-process comparisons.
Degenerate dimensionA dimension key (like ride_id) stored on the fact table with no separate dimension table.
Accumulating snapshotA fact table with one row per process instance that is updated as each milestone date is reached.

In two minutes

Test yourself

1. Which column belongs in a fact table rather than a dimension?

  1. Driver's vehicle model
  2. Pickup zone name
  3. Fare in ₹ for a ride
  4. Rider's signup channel
Show the answer

Fare in ₹ for a ride — Fare is a numeric measurement of the ride event you aggregate. Vehicle model, zone name and signup channel are descriptive attributes you group or filter by, so they live in dimensions.

2. Why do dimension tables use surrogate keys instead of the source system's id?

  1. Integers compress better, and that is the only reason
  2. So the warehouse can hold several versions of the same entity and stay independent of source id changes
  3. Because source ids are always strings
  4. Because BI tools cannot join on natural keys
Show the answer

So the warehouse can hold several versions of the same entity and stay independent of source id changes — The decisive reason is history and independence: one driver can have several rows (one per tier period), which cannot all share driver_id. Compression is a nice side effect, not the reason.

3. A driver's in-app wallet balance is stored daily. What kind of measure is it?

  1. Semi-additive
  2. Additive
  3. Non-additive
  4. Degenerate
Show the answer

Semi-additive — You can sum balances across drivers on one day (total liability), but summing one driver's balances across 30 days is meaningless. Additive across some dimensions, not time = semi-additive.

4. Finance wants each ride's lifecycle — requested, accepted, picked up, dropped, paid — in one row with five timestamps that fill in over time. Which fact shape?

  1. Transaction fact
  2. Periodic snapshot
  3. Factless fact
  4. Accumulating snapshot
Show the answer

Accumulating snapshot — One row per process instance, updated as milestones occur, is exactly the accumulating snapshot. A transaction fact would hold one row per event instead.

5. Analysts need rides by pickup date and also by payment date (payments settle up to 3 days later). Best design?

  1. Two date dimensions with different columns
  2. Store dates as strings on the fact and skip dim_date
  3. One dim_date joined twice via pickup_date_key and payment_date_key
  4. A separate fact table for each date
Show the answer

One dim_date joined twice via pickup_date_key and payment_date_key — That is a role-playing dimension: one conformed dim_date, used in two roles. Two different date tables would drift apart in definitions (fiscal quarter, holidays).

6. Rides and complaints are in different fact tables. The CEO wants complaints per 1,000 rides by city. What makes that possible cleanly?

  1. Both facts use the same conformed dim_city and dim_date
  2. Putting complaints as a column on fact_rides
  3. A cross join between the two facts
  4. Storing city names as text in both facts
Show the answer

Both facts use the same conformed dim_city and dim_date — Conformed dimensions allow drill-across: aggregate each fact by the shared city and date keys separately, then join the two small results. Joining the facts directly at row level would fan out.

There are 9 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →