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.
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.
↑ 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.
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:
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.
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.
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
- Facts happen, dimensions describeFacts are huge, finished and added to. Dimensions are small, and they change — which is where all the trouble comes from.
- 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.
- Store the finest detail you haveYou can always add up later. You can never get back detail you threw away.
- Type 1 rewrites the pastOverwriting a field reaches back and changes every historical report that used it. Nothing is deleted and nothing warns you.
- 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.
- Decide history column by columnWould an old report be wrong if this changed? Then keep history. A corrected typo is not history.
- Not keeping history is the one thing you cannot undoEverything else on this page can be rebuilt. A change you never recorded is gone.
- 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.
- 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…