Crosshire
Data model
Foundation02 Data model · 1 of 5

Choosing the shape

The normal forms first, because they are what silver obeys and gold deliberately breaks. Then the three modelling styles that get proposed in the first workshop — only one of which is what a serving layer is for.

Stand 29.07.2026

#Normal forms, and the layer that stops obeying them

What
Normalisation is one idea applied repeatedly: every non-key column should depend on the key, the whole key, and nothing but the key. The numbered forms are just that sentence taken in bites:
1NF — one value per cell
Every cell holds a single value, with no repeating groups.
2NF — nothing depends on part of the key
On a composite key, no column may depend on only part of it.
3NF — nothing depends on a non-key column
No column may depend on another non-key column.
BCNF — the overlapping-candidate-key case
Tightens 3NF for the rare table with overlapping candidate keys. In a warehouse you will almost never need to invoke it by name.
Each is easier to recognise as a smell than to recite as a definition, and all three appear in this warehouse's own sources:
Why
Two failures, and teams pick one. Normalise gold and the star dies under its own joins: a country breakdown becomes four hops, the metric view sprouts joins it was meant to remove, and every analyst learns to extract to Excel instead. Denormalise silver and the update anomalies arrive instead — the same customer's address corrected in one place and not the other, with no single row that is authoritative.
The reason this belongs in week one rather than in a modelling handbook is that both failures are invisible at design time and expensive at month four. Nobody looks at a diagram and sees the extract-to-Excel habit forming.
How
sql
The three violations, as they actually arrive from SAP and the webshop
-- 1NF: one cell, two facts. The webshop exports a line list as a string.
--   shop_order.items = 'SKU-1:2;SKU-9:1'
-- You cannot filter, join or sum it without parsing it first.
-- Fix: explode to one row per item -> silver.sales_order_line

-- 2NF: partial dependency on a composite key.
-- Key is (order_id, line_number), but order_ts depends on order_id ALONE.
--   order_id | line_number | order_ts   | quantity
--   1001     | 1           | 2026-03-04 | 2
--   1001     | 2           | 2026-03-04 | 1        <- order_ts repeated
-- Fix: order_ts belongs on silver.sales_order, the header, once.

-- 3NF: transitive dependency between non-key columns.
--   postal_code -> city -> country
-- Change one town's postal code and you must find every row that carries it.
-- Fix in silver: a location table keyed by postal_code.
That is silver's job, and the reason silver is source-shaped and historised rather than pretty: an update applied in one place cannot contradict itself, which is the whole return on normalising.
Gold then breaks the third form on purpose, and this is the part that gets argued about:
sql
gold.dim_customer keeps the transitive dependency deliberately
SELECT customer_sk, customer_name, postal_code, city, country, customer_segment
FROM   gold.dim_customer;

-- country is functionally determined by postal_code and stored anyway.
-- In 3NF terms this dimension is wrong. As a serving layer it is right:
-- a report grouping by country reads ONE table and joins nothing, and
-- nobody has to know a location table ever existed.
So the normal forms are not a quality ladder you climb as high as you can. They are a tool for the layer whose problem is update anomalies, and gold's problem is not updates — it is reads. A dimension is denormalised on purpose, and a reviewer who flags country in gold.dim_customer as a 3NF violation is correct about the form and wrong about the layer.
Doing it
  • State the target form per layer, in the developer concept, in one line eachSilver approximately 3NF and historised; gold denormalised star. Written down it survives the reviewer who has only ever normalised.
  • Fix 1NF violations at the bronze-to-silver boundary, never laterA packed string that reaches gold has already been parsed by three different people three different ways.
  • When you denormalise into a dimension, record which dependency you acceptedpostal_code determines city determines country. One comment on the column is enough, and it stops the annual re-litigation.
  • Do not normalise a dimension to reduce storageThe dimension is the small table. Snowflaking gold.dim_customer to save megabytes costs joins on every query that touches it.
Embedding it
  • Ask which layer, not whether, when someone says a table is not normalisedIt turns a style argument into a design question with an answer.
  • Have the team name the normal form each source violates before modelling itTen minutes, and it front-loads exactly the work silver exists to do.
  • Keep one worked 3NF-to-star example in the enablement materialsilver.customer becoming gold.dim_customer is the whole idea in one slide, using tables they already know.
Defaults
  • Silver approximately 3NF, historised, source-shaped
  • Gold denormalised: dimensions wide and flat, facts narrow
  • 1NF enforced at the bronze boundary — no packed strings past silver
  • Accepted dependencies commented on the column, not remembered
Gotchas
  • Treating 3NF as a quality score that gold should also earnIt is a tool against update anomalies. Gold does not update, it reloads — so the form buys nothing there and costs a join on every read. The reviewer is right about the definition and wrong about the layer, which is why the argument goes in circles.
  • Snowflaking a dimension because a modelling tool drew it that wayReverse-engineering a diagram from silver produces a snowflake by default. It looks tidier, reads slower, and analysts stop using it without ever filing a complaint — you find out from the extract volume, not from a ticket.
  • Answering "how is it modelled?" with "medallion"The layers are architecture; they constrain the shape of nothing. Both sides leave the workshop believing a modelling decision was taken, and it was not.
  • Leaving a 1NF violation because the parse looks cheap todayA packed string is parseable until the delimiter appears inside a value — and it will, in a product name, on a Friday, silently changing every line count downstream.

#Kimball, Data Vault, or one wide table

What
Gold here is a Kimball star: conformed dimensions with surrogate keys, facts at the finest grain the source delivers, aggregates derived from facts. A decision, not a default — and it gets challenged, usually by someone who has delivered Data Vault well.
The three candidates, judged as a serving layer
Kimball starData Vault 2.0One wide table
Core unitdim_ and fct_hub / link / satelliteone denormalised table per subject
Optimised forreading and aggregatingloading and auditingone dashboard
Objects here6 tables + 1 metric viewroughly three times thatone per report
History livesSCD2 in the dimensionsin satellites, every attribute, alwaysfrozen into each row
A new source costsmapping into the conformed dimensiona new satellite; nothing existing changesa rebuild
Analyst self-serviceyesno — a Vault is not a serving layeryes, until the second question
The vocabulary, which you need to use correctly to be taken seriously: a hub is a business key and its hash. A link is a relationship between hubs, carrying no descriptive data. A satellite holds the attributes of a hub or link with a load timestamp, insert-only.
Why
The failure this prevents is not choosing badly; it is choosing twice. A Vault and then a star means two definitions of a customer — discovered four months in, when the extract and the dashboard disagree by 0.4 % and nobody can say which is right. The wide-table version arrives faster: with no conformed customer to group by, the second report joins on customer_name, and DE/AT spellings of one company do the rest.
How
text
Illustration only — these Vault objects are NOT part of this warehouse
this warehouse, as a star        the same domain, as a Data Vault
------------------------------   -------------------------------------
gold.dim_customer   (SCD2)       hub_customer + sat_customer_sap
                                              + sat_customer_shop
gold.dim_product    (SCD2)       hub_product  + sat_product
gold.fct_order_line              hub_sales_order + lnk_order_line
                                              + sat_order_line
gold.agg_revenue_month           still to be built, on top of all of it
Doing it
  • Write the choice down in week one, with its rejected alternativesUnwritten, it is re-opened every sprint.
  • If a Vault exists upstream, treat it as silver and build gold on topProposing its removal is an argument you will lose, and should.
  • Model the star from the questions the business asksAnything nobody groups by is not a dimension.
  • Facts at source grain; agg_ tables derive from fct_, never from silver
Embedding it
  • Have them model one new source into the star while you review rather than drawThe webshop forces the conformance question immediately.
  • Run the Vault comparison with the Vault advocate presentingThey make the case better than you and own the outcome either way.
  • Ask in every design review which question the new table answers
Defaults
  • Kimball star in gold; silver stays source-shaped and historised
  • One decision record: the choice, the alternatives, the date, the author
  • Facts at the finest grain delivered; aggregates derived, never authored
  • If a Vault exists upstream, it is silver. Do not build a second integration layer
Gotchas
  • Running a Vault and a star as peersTwo revenue definitions, found when an extract and a dashboard differ by a fraction of a percent — a week of arbitration each time, recurring.
  • Adopting Data Vault for five sourcesObject count triples and the first business-visible number arrives a month later. It earns its cost at twenty sources with provenance obligations.
  • Wide tables with dimension attributes copied inRestating history means rewriting the table, so nobody does, and the trend chart applies today's segment to last year's revenue.
Platform facts on this page verified 29.07.2026 against the official documentation. Volatile claims are anchored to the currency register. This is section 1 of 5 in 02 Data model.