Data warehouse design
Model for the questions, not for the writes. A warehouse is loaded in batches and read by scans, so the shape that serves it is a narrow table of measurements with descriptive tables beside it — the opposite of the normalised shape that keeps writes cheap.
Why is a warehouse slow at fetching one row? It shelves its data the same way a library would if it arranged itself around the questions people ask at the desk rather than the order the books arrived in. One book takes longer; what got borrowed last winter takes seconds.
You have been handed a warehouse to design, or inherited one already running, and most of what you can find on the subject hands you a diagram instead of a reason: staging on the left, a core layer in the middle, marts on the right, arrows pointing forward. The diagram is not wrong, but it does not explain why any of those boxes exist rather than one. The reason underneath it is the workload. A system that is written to constantly, under a user’s hand, and read back a few rows at a time by key, is a different machine from one loaded on a schedule and read by scanning most of a column — and every decision that follows, from how a table is shaped to how a load reruns, is downstream of which of those two machines is actually being built.
The workload is the whole argument#
Put the two workloads side by side and the shape argues for itself before a single table gets named. The traditional label for the first is OLTP, online transaction processing; for the second it is OLAP, online analytical processing. The names sound like they describe two different technologies, but the distinction they draw is about what a system is asked to do, not about which product does it — the same relational engine can run either workload, and what actually changes is the pattern of reads and writes it sits under.
| Aspect | Operational system (OLTP) | Warehouse (OLAP) |
|---|---|---|
| How data arrives | One row at a time, under a user’s hand, as the transaction happens. | In scheduled batches, loaded after the fact from wherever it happened. |
| How it is read | A few rows, found by key — one order, one account, one session. | Most of a column, scanned and aggregated across a wide slice of history. |
| What correctness demands | A transaction that must not tear — a write lands whole or not at all. | A load that must be repeatable — running it again must not change the answer. |
| What the schema optimises for | Cheap writes and no duplication. | Cheap scans and few joins. |
Normalisation is what buys the left column its cheap, non-duplicated writes: it makes every fact live in exactly one place, so a single write touches one row and nothing in the schema can disagree with itself. A warehouse does not have that problem. It has the opposite one — the row was already written, checked and settled, somewhere else, and the only work left is answering a wide, repeated question against many of them cheaply. That is the sentence the rest of this article hangs off: model for the reads, because the writes are already someone else’s problem.
What you decide before you write any SQL#
Four decisions get made before a single CREATE TABLE runs, and skipping one does not avoid it — it just means the decision gets made by accident, later, by whichever query happens to run first.
The grain of each fact table
The grain is the plain sentence describing what one row means — one order line, one session, one reading per sensor per hour, whatever the source process actually produces. It gets written down before a column is chosen, because every dimension that can attach and every measure that can be summed has to agree with that sentence, or the numbers it produces answer a question nobody asked.
Which dimensions are shared across fact tables
A dimension used by more than one fact table — customer, product, shop — has to mean the same thing in both, because comparing two fact tables really means comparing them through the dimension they share. Two fact tables that each keep their own private idea of what a shop is can be joined without any error and still produce numbers that do not agree, with nothing in the schema to say why.
What happens when a descriptive value changes
A customer moves region, a product changes category — someone has to decide whether the dimension row is overwritten in place or kept as a new version alongside the old one. That is a business question wearing a modelling costume: overwrite it, and last year’s report, rerun today, quietly starts saying something different than it said last year; version it, and the report stays put, at the cost of a dimension that now carries a history of its own.
How a load re-runs safely
A load will re-run — a job restarted after a failure, a backfill for a date that arrived late, a full reload once a bug upstream is fixed. A load that is only correct the first time it runs is a load that fails once and then fails differently every time after, because each rerun adds instead of replacing. Scoping a load so that running it twice for the same period leaves the same table is a modelling decision, not an operational afterthought.
-- Scope both statements to the same partition key so the load can
-- run once or ten times for the same date and leave the same table.
DELETE FROM fact_orders
WHERE order_date_key = :run_date_key;
INSERT INTO fact_orders (
order_date_key, customer_key, product_key, quantity, revenue
)
SELECT
order_date_key, customer_key, product_key, quantity, revenue
FROM staging_orders
WHERE order_date_key = :run_date_key; The pattern buys repeatability, not speed: deleting the target partition before inserting it means a rerun replaces what was there instead of adding to it, so a job that fails halfway through and gets restarted cannot leave the table holding two copies of the same date.
Where it goes wrong#
The warehouse gets built by copying the production schema and pointing a reporting tool at the copy. It is not a bad start, and for a while nothing about it looks wrong — the tables are already there, the joins already work, and a dashboard goes live fast. It keeps working while the data stays small and the questions stay close to the ones the operational schema already answered well.
A report that joins many tables gets slower every week
A question that used to be a short, direct join now works through most of the schema to get there, because nothing in a copied operational model was ever shaped to answer analytical questions in few joins. Each new question adds another join to an already long chain, and the chain gets walked in full on every run.
Two teams compute one metric differently, because there is no agreed grain
Nothing in a copied schema states what one reporting row of “an order” is supposed to mean, so two teams write two queries that both look reasonable and disagree. Neither team is wrong about their own query — there was never a grain for either of them to be wrong against.
A schema change on the transactional side silently breaks the copy
A column gets renamed, a table gets split, a status value gets added — all for reasons that have nothing to do with reporting — and every downstream query built against the old shape either breaks outright or, worse, keeps running and quietly answers a slightly different question than it did the day before.
The copy was never a warehouse. It was a second database with the same shape and none of the guarantees — no agreed grain, no versioned dimensions, no load that reruns safely — and the cost of not choosing a model up front does not disappear. It gets paid later, usually by whoever is debugging a dashboard that used to be right.
None of this makes copying production the wrong first move. It is often exactly the right one — cheap, fast, good enough for a while, before the real questions are fully known. What makes it wrong is leaving it there once somebody starts making a decision from what it says, because that is the moment an unmodelled copy stops being a shortcut and starts being a liability nobody agreed to take on.
Optimising for reads at the expense of writes, or the other way round, is not a decision unique to how a warehouse’s tables are modelled — the article on two storage engines built around opposite assumptions about reads and writes covers the same fork one layer down, inside a single engine rather than across a schema. The persistence path is the roadmap this article aligns with most closely.
IF YOU REMEMBER ONE THING
Model for the questions the warehouse has to answer, not for the writes it will never take directly. The grain, the shared dimensions, how a changed value is kept, and how a load reruns are the four decisions that shape follows from — skip them and they still get decided, just by accident, and later.
Questions people also ask
5 QUESTIONSWhat is the difference between a data warehouse and a database?
"Database" usually means the operational one behind an application — normalised, tuned for many small transactional writes and reads by key. A warehouse is read by scans across history by several people asking overlapping questions at once, and its schema is shaped for that instead, often by loading from one or more operational databases rather than replacing them.
Do I need a warehouse if I only have one application?
Not from day one. One application's own database can usually answer its own reporting questions well enough on its own. The signal to build a warehouse is combining data from more than one source for analytical reading, or noticing that reporting queries are competing with the transactions the application depends on.
What is a data mart?
A narrower model built for one team's or one subject's questions, usually loaded from the wider warehouse rather than straight from source systems. It exists so an analyst answering one department's recurring question does not have to join across the whole model every time to get there.
Should the warehouse hold raw data or cleaned data?
Often both, in separate tables. A layer close to the source keeps data close to what actually arrived, so a transformation that turns out wrong can be redone without going back to the source system. Cleaned, modelled tables serve the real questions. Skipping the raw layer trades recoverability for a shorter pipeline.
How is a data lake different from a warehouse?
A lake keeps data in whatever shape it arrived in, often with no schema imposed until it is read. A warehouse imposes the model — the grain, the dimensions — before loading it. The two are not competitors so much as different stages: a lake holding raw arrivals with a modelled warehouse layer built on top is a common pairing.