← All casesCase 0415 min

Case 04 · Dimensional modelling

A customer moved house, and last year’s revenue moved with her.

Nobody edited a number. No job failed. Bengaluru is simply ₹2,000 poorer than it was last week, and Pune is ₹2,000 richer, for a year that already finished. This is the single most common way a warehouse quietly lies, and it has a name.

First, the two kinds of table

Things that happen, and things that just are.

Every warehouse table is one of two kinds, and once you see it you cannot unsee it.

A fact

Something that happened

order 8841
2026-01-14 19:22
customer C-77
₹1,200

It has a time, and it has numbers you add up. It happened once and it never happens again. There are billions of these and they only ever get added to.

A dimension

Something that describes

customer C-77
Ananya
Bengaluru
joined 2024

It answers who, what, where. You do not add these up — you slice by them. There are far fewer, and unlike facts, these change.

Almost every question a business asks is the same shape: add up a fact, split by a dimension. Revenue by city. Orders by month. Refunds by product category. Once you notice that, the reason for splitting tables this way stops being academic.

And the trouble in this whole case comes from one line above: facts are finished, dimensions are not.

The question before all others

“One row means what, exactly?”

Before you design anything, you answer that sentence for the fact table, out loud, in one line. One row per order line. One row per shipment. One row per day per product per store.

That sentence is called the grain, and getting it wrong is the most expensive mistake on this page, because everything you build afterwards inherits it.

Too coarse and the question cannot be asked. One row per order, and somebody wants revenue by product. The detail is gone. You cannot get it back by being clever — you have to rebuild.
Mixed grain and the numbers are wrong. Some rows are per order, some per line item. Now every total double-counts something, and it looks fine, because all the individual rows look fine.
The safe default is the finest detail you have. You can always add up. You can never un-add.

If you take one habit from this case, take this one: write the grain down in a comment at the top of the model, in plain words, before writing any SQL. Half of all modelling arguments end the moment somebody does that.

The thing that breaks

She lived in Bengaluru. Then she moved. What happens to her old orders?

Ananya is customer C-77. She ordered twice from Bengaluru, moved to Pune in May, and ordered twice more. Four orders, ₹4,400 in total, and nobody disputes any of that.

Now the business asks a completely ordinary question: how much revenue came from each city this year? The answer depends entirely on how you chose to store her address — and two of the three choices below give you a wrong answer without telling you.

how the customer table handles a change
Jan · ₹1,200Bengaluru
Mar · ₹800Bengaluru
Jul · ₹1,500Pune
Sep · ₹900Pune

↑ what actually happened · the amber line is the day she moved

the customer table now looks like this

so “revenue by city this year” returns

Why it is worth the trouble

Type 1 does not lose her address. It rewrites the past.

That is the part that catches people. Overwriting looks harmless — the customer table is more up to date than it was. Her address is correct. Nothing is missing.

But the orders from January and March do not carry a city of their own. They carry her id, and they look the city up when the report runs. So changing one field in one row silently reached back and moved ₹2,000 of finished history into a city it never happened in.

Nothing was deleted. The past simply started giving different answers.

Now scale it. A customer who changes from the free plan to paid, a product that moves category, a salesperson who changes region, a store that gets reassigned to a new area manager. Every one of those, stored as Type 1, quietly rewrites every report that ever touched it.

This is also why last quarter’s numbers sometimes refuse to match the deck from last quarter, and why nobody can work out who changed them. Nobody did.

How Type 2 actually works

Three columns and a new row. That is the whole mechanism.

Type 2 does not update anything. When something changes, it closes the old row and opens a new one.

The columns you add

Valid from, valid to, current

customer_key   9102      ← new each version
customer_id    C-77      ← stays the same
city           Pune
valid_from     2026-05-04
valid_to       9999-12-31
is_current     true

The old row keeps everything it had, gets a valid_to of the day she moved, and its flag turns false.

How the order finds the right one

Match on the date, not just the id

join customers d
  on f.customer_id = d.customer_id
 and f.ordered_at >= d.valid_from
 and f.ordered_at <  d.valid_to

A January order lands on the Bengaluru version because January falls inside that row’s window. A July order lands on Pune. Same query, correct answer, no special cases.

Two details that trip people up. The customer_key is new for every version and means nothing to anyone — it exists only so each version has its own identity; people call it a surrogate key. And the is_current flag is a convenience, not the truth: it makes “who are they now” easy, but if you use it for historical reporting you have quietly rebuilt Type 1 with extra steps.

The other three types, briefly, so the words are not a mystery. Type 0 is “this can never change” — a date of birth, the date an account opened. Type 4 keeps today’s values in a small fast table and shoves all the history into a second one beside it. Type 6 is 1 and 2 and 3 together, so you can ask both “where did she live then” and “where does she live now” in one query. You will meet Type 1 and Type 2 constantly, Type 0 occasionally, and the others mostly in interviews.

So use Type 2 everywhere?

No. It costs, and most fields do not deserve it.

Every Type 2 field means the dimension grows a row every time anything about that customer changes. Track ten volatile fields on a ten-million-customer table and you have built something enormous, slow, and unpleasant to query.

So decide column by column, and the test is simple:

Would a report from last year be wrong if this value changed? City, plan, segment, region, category. Track history.
Or is the old value just noise? A typo in a name, a corrected phone number, a fixed spelling. Overwrite it. Nobody wants a permanent record of a misspelling.

Most real dimension tables are a mix — a few Type 2 columns that matter for history, and Type 1 for everything else. That is normal and correct, not a compromise.

The mistake worth avoiding: choosing Type 1 because it is easier, and finding out eighteen months later that the history you needed was never kept. You cannot backfill a change you did not record. That one is genuinely unrecoverable.

The shape it makes

One fact in the middle, dimensions around it.

Draw a fact table with its dimensions hanging off it and you get something that looks like a star, which is why everyone calls it that.

customerwho
productwhat
ordersthe fact
storewhere
datewhen

The reason to split them is not tidiness. It is that facts and dimensions have opposite lives. Facts are enormous and never change. Dimensions are small and change constantly. Keeping them apart means a customer changing address touches one row, not forty million.

You will also hear about a snowflake, which is the same thing with the dimensions themselves split further — city pulled out of customer, category pulled out of product. It saves space and costs joins. On today’s warehouses that trade has mostly stopped being worth it, for a reason you already know from Case 01: repeated values compress to almost nothing, so the space you save was never really being used.

The modern argument

“Why not just flatten it all into one wide table?”

This is a real debate right now, not a settled one, and you should be able to argue both sides. The case for one big table is decent: warehouses are columnar, unused columns cost nothing to skip, and analysts stop having to learn a model before they can ask anything. Pick a situation and see how each one behaves.

star schema

one big table

Notice that neither column wins outright, and notice where each one wins. One big table is better at being read. The star is better at being changed.

Which is why the answer most teams land on is not a choice at all: keep the star as the thing you maintain, then build wide flat tables on top of it for the handful of dashboards that get hammered. The flat tables are disposable — if a definition changes you rebuild them from the star. Build only the flat tables and you have nothing to rebuild them from, and you get the problem in the next chapter.

Three teams, three answers

Everyone has a number for active customers. None of them match.

A data mart is just a slice of the warehouse shaped for one team — finance gets theirs, marketing gets theirs. Useful, until each team quietly defines the same word differently. Change what each one counts and watch the meeting go wrong.

Nobody here did anything wrong. Every team built a sensible table for its own use. The problem only appears in the one meeting where all three numbers are on the same slide, and then it costs a fortnight of arguing about whose number is real.

The fix has an ugly name and a simple idea. A conformed dimension is one customer table, one product table, one date table, shared by every mart — same keys, same names, same meaning. Marts can still hold whatever facts they like. They just may not invent their own version of what a customer is.

And when teams genuinely need different definitions — which happens — the answer is two clearly named columns in the shared table, not two private tables that both call it “active”.

Three things people get wrong

Including one that sounds like good practice.

“We will add history later if we need it.” You cannot. A Type 1 column has no record of what it used to be, so the day someone asks for last year’s split, the honest answer is that it no longer exists. This is the only decision on this page that is genuinely irreversible, which is exactly why it deserves thought on day one.
“Normalise everything, duplication is bad.” That instinct comes from app databases, where duplication really is dangerous because things get updated in place. A warehouse mostly adds rows and rarely edits them, and repeated values compress to nearly nothing. Copying a category name into a million rows is not the sin here that it is over there.
“The model is the diagram.” The diagram is the easy part. The model is the agreement about what the words mean — what counts as an order, when a customer becomes active, whether a cancelled sale is revenue. That argument is the actual work, and no tool settles it for you.

If you get asked

“How would you model orders for an e-commerce company?”

Say the grain first. “One row per order line, because people will want revenue by product.” One sentence, and you have already sounded like somebody who has done this.

Then name the dimensions and what changes in each. Customer, product, store, date. Then say which fields need history — city and plan for the customer, category for the product — and which you would happily overwrite.

Then bring up the thing they are hoping for. That if you overwrite the customer’s city, last year’s revenue by city changes, so those columns are Type 2 and the fact joins on the date range rather than just the id. That one sentence covers modelling, history and correctness together.

Then mention what you would not do. Not Type 2 on everything, because the table explodes. Not a private customer table for each team, because the numbers stop matching.

If they push on one big table, do not pick a side. Say the flat table is a serving layer and the star is what you keep, and explain why in terms of what happens when a definition changes.

Two questions

Did it land?

A customer changes from the free plan to paid. You store plan as Type 1. What breaks?

Finance and marketing report different active customer counts for the same quarter. Most likely cause?

Your playbook · Case 04

Nine things to keep

  1. Facts happen, dimensions describeFacts are huge, finished and added to. Dimensions are small, and they change — which is where all the trouble comes from.
  2. Write the grain down first“One row per what?” in plain words, before any SQL. Too coarse and the question can never be asked. Mixed and every total is wrong.
  3. Store the finest detail you haveYou can always add up later. You can never get back detail you threw away.
  4. Type 1 rewrites the pastOverwriting a field reaches back and changes every historical report that used it. Nothing is deleted and nothing warns you.
  5. Type 2 is a new row, not an updateClose the old version with a date, open a new one, and join the fact on the date range instead of just the id.
  6. Decide history column by columnWould an old report be wrong if this changed? Then keep history. A corrected typo is not history.
  7. Not keeping history is the one thing you cannot undoEverything else on this page can be rebuilt. A change you never recorded is gone.
  8. Flat tables serve, the star survivesWide tables are fast and rigid. Keep the star as the thing you maintain and rebuild flat tables from it.
  9. One customer table for everyoneShared dimensions with shared meanings. Private copies are how three teams end up with three numbers and a very long meeting.

Your review

How was this case?

Loading ratings…

Next · Case 05 · drops soon

Where did this number come from?

Case 05 follows one wrong figure backwards through every table it touched — so the next time something looks off, you are hunting for minutes rather than all day. Leave your email and we’ll tell you the day it goes live.

← Case 03All cases