Data warehouse modelling standards
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
1. Scope and triggers
Applies to deciding the shape of the analytical destination: which tables exist, at what grain, with which
keys, how history is kept, how they are organised into layers and where the definition of a
metric lives.
Triggers: fct_/dim_/stg_/int_, schema.yml/semantic_models/metrics:, hub_/
lnk_/sat_, _sk/_key/surrogate, valid_from/valid_to/is_current/dbt_valid_to,
dim_date/dim_calendar, unique_key, "star schema", "grano", "grain", "SCD2", "historise",
"conformed dimension", "OBT", "wide table", "marts", "gold layer", "medallion", "metric",
"semantic layer", and the questions that give away a badly declared model: "why does revenue not
match between these two reports?", "how many rows should this table have?", "what does
a row represent here?", "why does the sum come out double?", "we want to see how the customer looked in
March".
Not applicable: see
data-engineering-standards (sibling; boundary declared on both sides): it moves and
transforms —ingestion, ELT/ETL, orchestration, idempotency, backfill, Parquet, scan
cost, freshness and data observability—; this one decides the shape of the destination. Cutting
rule: if the question is "how do I reload March without duplicating?", it is theirs; if it is "what
does a row represent and what happens when the customer changes segment?", it belongs here. A pipeline
without a model produces a swamp; a model without a pipeline is a diagram.
data-platform-standards (parent): PostgreSQL as an operational engine is theirs, with its
normalised modelling, its indexes, its migrations and its partitioning. Here the analytical
destination is modelled, which is something else and obeys other rules. Its guiding principle —a warehouse by
need, not by fashion— is inherited: this skill does not authorise a new analytical warehouse; it assumes
it was already justified. A dim_ is not created inside the transactional database.
lakehouse-standards: the table format —Iceberg, Delta, Hudi—,
file-level schema evolution, hidden partitioning, time travel and engine snapshots. A fine and
deliberate boundary: the table format's time travel gives you back the table
as it was; an SCD type 2 tells you how the business entity was. They are not substitutes:
the first is an infrastructure capability for recovery and technical audit, the second
is a modelling decision that the business queries. Never replace an SCD2 with time
travel.
analytics-bi-standards: the BI tool, the dashboards, the
governance of consumption and the performance of the presentation layer. Explicit boundary
decision: the canonical definition of a metric belongs here (§3.7) —it lives in the repository,
versioned, next to the model—; BI consumes it, it does not define it. A metric defined inside
a report is a modelling defect, not a tooling choice.
data-governance-quality-standards: catalogue, ownership, business
glossary, contracts and quality as a programme. Here the structural tests of the model (§4).
streaming-cdc-standards: change capture as a mechanism. Here only
the use it is put to: feeding dimensional historisation.
microservices-architecture-standards: domain events, outbox and per-service data ownership
are theirs. A domain event is not an analytical fact: modelling the fact belongs here.
privacy-engineering-standards: retention, erasure, minimisation, pseudonymisation and
personal data classification are theirs; here they are executed in the model —which columns do not
enter a dimension, how a subject is erased from an SCD2 without destroying the facts, where
the mapping table lives—.
object-storage-standards (S3 as the substrate), observability-standards, mlops-standards
(feature store and train/serve skew are theirs; a features table is not a mart),
rag-standards, ai-governance-standards, grc-compliance-standards, bcdr-standards,
backup-recovery-standards, cicd-standards, iac-standards,
identity-access-management-standards, secrets-management-standards,
aws-standards/azure-standards/gcp-standards (Redshift, BigQuery, Synapse/Fabric as
managed services: cost, networking, IAM and provisioning are theirs; the modelling criteria
on top belong here), api-design-standards.
- Non-analytical engines:
nosql-standards, graph-db-standards, timeseries-db-standards,
search-engines-standards, oracle-dba-standards, sqlserver-dba-standards,
mysql-mariadb-dba-standards.
sql-standards (the SQL language, including the SQL contained in a dbt/SQLMesh model).
Boundary mirrored from its §1: "what does a row represent?" —grain, facts, dimensions, SCD,
layers, canonical metric definition— belongs here; "how do I express that SCD2 in SQL without a
correlated subquery?" is theirs. SQL living inside a model does not exempt it from
the language criteria.
Domain thesis — modelling still matters. The argument that cheap storage
and elastic compute retired modelling confuses the cost that disappeared with the ones that remain.
Disk stopped mattering; compute, comprehension and trust did not:
- Compute: in modern engines you pay per scan. A table with no declared grain is
queried badly and paid for every time.
- Comprehension: if nobody knows what a row represents, every analyst invents their interpretation and
all of them are plausible.
- Trust: it is the scarce resource. Two reports that disagree destroy trust in the
platform faster than an error that is detected and fixed — an error gets fixed; the
discrepancy teaches the organisation that data is a matter of opinion.
What has changed is the implementation: fewer physical star schemas built by hand, more
tool-managed models and semantic layers on top. The obligations that survive are
the same: declare the grain, separate fact from dimension, conform dimensions and historise what
changes.
2. Default decisions
Verify on the web the status of the references, specifications and tools before pinning them
in a real project (§8).
2.1 Choice of paradigm
| Paradigm |
When it is the right answer |
Real cost |
Verdict |
| Kimball / dimensional (star) |
Default. The destination is queried to answer business questions |
Requires declaring the grain and conforming dimensions — intellectual work, not technical |
Adopt this unless there is a written reason |
| Inmon / corporate 3NF |
Integration of many systems with complex business rules and a central team that sustains it, feeding dimensional marts downstream |
Slow to deliver; incomprehensible for the end consumer: it is never the consumption layer |
Intermediate layer in large organisations |
| Data Vault 2.0 (hubs, links, satellites) |
Many changing sources and a requirement for full auditability: who said what, when and from which system (banking, insurance, regulated sector) |
Table explosion (of the order of 3× against the equivalent relational model) and unmanageable joins; it forces PIT and bridge tables just to be able to query; and building dimensional marts on top anyway |
It solves an audit and integration problem most people do not have. The adoption data confirms it: adopters themselves cite as the main drawbacks training needs (48 %), implementation complexity (35 %) and query performance (~32 %), and only ~31 % say their implementation fully meets the standard. It requires automation and trained staff |
| One Big Table (OBT) |
Serving layer, derived from a model, for a dashboard or a specific case (§3.6) |
It is rebuilt in full; it specialises and multiplies |
Correct as a destination, never as the source of truth |
Rule: Data Vault without an explicit audit requirement is over-engineering, and specifically the
worst kind of over-engineering: the kind that imposes a permanent explanation tax on everyone who
joins the team.
2.2 Modelling toolchain
| Area |
Default |
Note (Aug 2026) |
| Canonical dimensional reference |
Kimball & Ross, The Data Warehouse Toolkit, 3rd ed. (2013) |
There is no 4th edition; the Kimball Group ceased its consulting/training activity in 2016. It is still the reference, with the caveat that its ETL chapters describe a pre-ELT era |
| Metric definition |
In the repository, in versioned YAML, next to the model |
MetricFlow relicensed to Apache 2.0 (Oct 2025). Cube as a headless alternative (it defines and serves: they are not interchangeable) |
| Semantic interchange specification |
Apache Ossie (incubating), formerly Open Semantic Interchange (OSI) |
Apache-2.0, JSON/YAML specification. It is a specification, not a product: it stores nothing and serves nothing. Watch the real adoption of importers before betting on portability |
| Model tests |
Versioned declarative assertions (uniqueness, not null, relationships, accepted values) |
Execution gate (§4) |
| Grain documentation |
In the model itself, not in a wiki |
A grain documented outside the code stops being true within a quarter |
3. Structure and conventions
3.1 Layers
Three, with separated responsibilities. The names vary (bronze/silver/gold,
staging/intermediate/marts); the rules do not:
| Layer |
Content |
Hard rule |
| Raw / bronze |
Faithful copy of the source, immutable |
It is not modelled. It belongs to data-engineering-standards |
| Intermediate / silver |
Typing, deduplication, entity conformance, business rules |
This is where the ugly logic lives, and only here |
| Consumption / gold / marts |
Facts and dimensions, or the OBT derived from them |
It must be boring: stable names, stable types, zero surprises |
That the consumption layer is boring is a requirement, not a style. It is the layer people read without
asking you, the one that breaks reports when it changes and the one that sets the contract with the business. Every
surprise (a column that is sometimes null, a status with new values and no warning, a metric
whose definition changed silently) is paid for in trust, and trust is not recovered with a
hotfix.
Forbidden to skip the intermediate layer by putting business logic in the consumption layer: the
result is marts that contradict each other because each one reimplemented the same rule slightly
differently.
3.2 Facts: the grain is the decision
The grain is the most important decision and the one most often got wrong. It is declared first, in a
natural-language sentence, before writing a single column:
"One row = one line of an order at the moment it was confirmed."
Rules:
- The grain is written in the model (the table's description), not in anybody's head.
- Atomic grain by default: the finest the source allows. Aggregating afterwards is trivial;
disaggregating is impossible. Pre-aggregates are derived optimisations, not the fact.
- One grain per table. Mixing order lines and order headers in the same table is the
root cause of half of the industry's inflated figures: any sum multiplies by the
number of lines.
- The grain is tested, not believed (§4): if the declared key is not unique, your grain is not what you
think it is, and everything built on top is wrong.
Measure types — the classification decides which aggregations are legal, and it must be declared:
| Type |
Sums over any dimension |
Example |
Trap |
| Additive |
Yes |
Sales amount |
— |
| Semi-additive |
Yes, except over time |
Account balance, inventory level |
Summing 12 months of balances gives a meaningless figure. Over time: last value or average |
| Non-additive |
No |
Ratios, percentages, unit margins |
Never store the ratio: store the numerator and denominator and divide when aggregating. Summing ratios is always incorrect |
Fact table types:
- Transactional: one row per event, atomic grain. The default.
- Periodic snapshot: one row per entity and period (daily balance, weekly inventory). For
semi-additive measures and for "how much was there" questions. It grows predictably.
- Accumulating snapshot: one row per process with multiple milestones, which is updated as it
progresses (ordered → paid → shipped → delivered). To measure latencies between phases. It is the only
fact table that gets updated, and it must be stated explicitly because it breaks the expectation
of immutability.
- Factless: it records that something happened or that a relationship exists, with no measure.
Attendance, coverage, eligibility. Legitimate and heavily under-used: the usual alternative is
inventing a
count = 1 column, which is the same thing with a worse name.
In addition: degenerate dimensions (order/ticket number) live in the fact, with no table of their own;
junk dimensions group low-cardinality flags that do not deserve one dimension
each.
3.3 Dimensions
- Surrogate keys in historised dimensions (SCD2) — they are mandatory there, because
the natural key stops being unique once there are several versions. In non-historised dimensions, the
natural key is acceptable and simpler; a conscious decision, not an automatism.
- The natural key never disappears from the model: it is kept as a column, because it is the only thing
that allows reconciling with the source system when someone asks.
- Never use the surrogate key as an integration key between systems nor expose it to the
outside: it is an internal detail of the warehouse.
- Every dimension has an "unknown"/"not applicable" row with a fixed key (typically
-1).
Without it, facts with a missing dimension are lost in the INNER JOIN and the figures drop without
anyone knowing why. This is a LEFT JOIN that silently unbalances the total.
- Hierarchies: inside the dimension itself (country → region → city) as long as they are fixed.
Ragged or variable-depth hierarchies require a bridge table — and they are the
most frequent excuse for complicating a model that did not need it.
Slowly changing dimensions (SCD) — Kimball defines types 0 to 7; in practice
only three are used, and the choice is made by business question, not by habit:
| Type |
Behaviour |
When |
| 0 |
The attribute never changes |
Original sign-up date, country of birth |
| 1 |
Overwrite |
The default. The business does not need the previous value (fixing a typo, current phone number) |
| 2 |
New row with valid_from/valid_to/is_current |
When the business needs to analyse the past with the values of that time: change of customer segment, of sales territory, of tariff |
| 3 |
A "previous value" column |
A real niche: a one-off reorganisation where you must report by the old structure and the new one at the same time. It is not a general alternative to type 2 |
Decision criterion, in one question: "when this attribute changes, should historical reports
reflect the value of that time or the current one?" If it is "of that time", type 2. If it is "the current
one", type 1. If nobody can answer, it is type 1 — and the fact that it was asked is documented.
SCD type 2 is not free and that is why it is in the prohibitions without further need: it multiplies rows,
forces every join from the fact to use the surrogate key valid at the event's date
(not the current one), breaks naive counts of the dimension (COUNT(*) stops being "number of
customers") and complicates erasure under the right to be forgotten. Apply it to the attributes that require it,
not to the whole dimension.
Conformed dimensions — the mechanism that makes two marts comparable. A conformed
dimension is the same dimension (same keys, same attributes, same meaning) used by
several facts. Without them, "sales by region" and "returns by region" cannot be crossed
because "region" does not mean the same thing, and nobody discovers it until the figures do not match in a
meeting. Rule: one business entity = one dimension = one owner. If a mart needs
a variant, it is justified in writing or the dimension is conformed.
3.4 The date dimension
A physical table, populated in advance, with one row per day (and another per hour/minute if needed,
separate). It is not optional and it is not replaced by the engine's date functions: it exists to
hold what the engine does not know —holidays, fiscal calendar, working weeks, seasons— and so
that queries filter by attribute instead of by expression.
- Key in
YYYYMMDD format (integer) or a date: readable in the fact, sortable, no join to
debug.
- Fiscal calendar as its own columns (
fiscal_year, fiscal_quarter, fiscal_period):
the fiscal year almost never matches the calendar one, and reimplementing it in every query guarantees
that some will match and others will not.
3.5 The date problem — local time versus UTC
A recurring source of discrepancies between reports that nobody can explain. A rule in three parts,
all mandatory:
- Always store the instant in UTC with a zoned type (
timestamptz/TIMESTAMP with zone).
Never a type without a zone: "2 pm" with no zone is an incomplete datum disguised as a complete one.
- Also store the business date key (
date_key) already resolved in the business
time zone, decided and documented. It is the column you filter and group by. Without it,
every query converts in its own way and "yesterday's sales" gives three different figures depending on who
asks.
- Document what the business zone is and what happens with multi-country operations: either a single
canonical zone, or a local
date_key per country in addition to the canonical one — chosen
explicitly and written in the model.
Additions: store the offset if the daylight-saving change matters (23- and 25-hour
days exist and break annual reconciliations); and decide which date rules —event date, posting
date, ingestion date— per fact. The three are different and all three are necessary; the
one used to partition is not necessarily the one used to report.
3.6 Deliberate denormalisation: OBT
Flattening everything into a wide table is correct when: (a) it serves a specific consumption case,
(b) it is derived from a dimensional model that still exists, (c) its rebuild is
automatic, and (d) it is documented as a serving view.
The gain is real and measurable —columnar MPP engines favour the wide table over the join
at query time—, but it is a last-mile gain.
It is laziness when: it replaces the model instead of being derived from it, every team has its own with
different rules, nobody knows what grain it is at, or the metric is defined inside the table itself and
gets duplicated in the next one. Unmistakable symptom: there are three wide "sales" tables and none
matches the others.
3.7 Metrics and the semantic layer
The definition of "revenue" or of "active customer" is a repository artifact, versioned,
reviewed by pull request and with an owner. It does not live in a report, nor in a spreadsheet,
nor in the head of whoever has been there longest.
- One definition, one name. If the business has two notions of revenue (gross and net of
returns), they are two metrics with two names, not one ambiguous metric.
- The metric is defined over the dimensional model, declaring the measure, the aggregation and the
dimensions it is legal to break it down by. A metric with no declared dimensions will be
broken down by something that makes no sense.
- The semantic layer does not fix a bad model: if the grain is wrong, the metric computed
on top is wrong with more steps and better presentation.
- Portability, with measured scepticism: Apache Ossie (incubating) is the interchange
specification, and MetricFlow (Apache 2.0) its reference declarative format. The specification
does not carry the serving architecture —cache, pre-aggregates, multi-tenancy, APIs—: that is not
portable even if the definition is. Adopt the specification out of hygiene; do not assume it as
a guarantee of exit.
- A metric with no owner = a dead metric. When two reports disagree, there has to be
someone who decides which is correct, and within how long.
4. Quality and testing — gates
A model's tests are not a matter of opinion: they are what proves the model is what you say
it is. In increasing order of cost; all of them are gates on publishing the consumption layer
unless stated otherwise.
- Uniqueness of the grain key. The test that proves the declared grain is real. If
the key is composite, the combination is tested. Without this test there is no model, there is a table.
- Not null on keys, on dimension foreign keys and on the columns a metric
depends on.
- Referential integrity: every dimension key in the fact exists in the dimension. Analytical
warehouses do not enforce FKs; therefore it is tested, or it does not exist.
- Accepted values on catalogue columns (
status, type, channel): it catches the new
value the source introduced without warning before it shows up as "other" in a report.
- Ranges and signs: amounts that cannot be negative, dates that cannot be in the
future, bounded quantities.
- SCD2 integrity — systematically forgotten and the place where things break most silently:
- Exactly one current row (
is_current) per natural key.
- The
[valid_from, valid_to) intervals of the same natural key do not overlap and leave no
gaps.
- The first version starts at the beginning of time and the current one has no end (or has a
documented sentinel date, always the same one).
- Reconciliation with the source: row count and sum of a key measure against the source system,
with a declared tolerance. It is the only test that detects row loss upstream.
- Reconciliation between layers and between marts: the canonical metric computed from the atomic fact and
from the aggregate/OBT must match. If two marts publish the same metric, a test compares
their results — this is the test that prevents the domain's most expensive failure.
- Non-multiplication (fan-out) test: after the model's joins, the fact's row
count does not change. It catches the join with a dimension that was not unique, which is the
mechanical cause of inflated figures.
- Unit tests of the business logic with fixed input rows and expected output,
covering edges: nulls, an SCD2 version change exactly at the instant of the event, a row arriving
out of order, an entity with no dimension, a zero amount, a full refund.
- Consumption-layer contract: removing or renaming a published column, or changing the
type, breaks the build unless there is explicit recorded approval. The consumption layer is an API.
- Documentation as a gate: a model with no grain description and no documented columns does not
merge.
5. Security and personal data in the model
- Classify while modelling, not afterwards. Every column of a dimension knows whether it is public, internal,
confidential or personal. The classification decides who sees it and how long it lives
(see
privacy-engineering-standards).
- Minimisation in the model: a dimension is not a dumping ground for every field in the source.
Every copied attribute is an attribute you have to retain, erase and explain in a breach. Copying
the whole
customer table "just in case" is a privacy decision taken by omission.
- The right to be forgotten and SCD2 collide head-on: erasing a subject from a historised
dimension destroys the traceability of the facts if it is done naively. Design from the
start: identifying attributes separated from the analytical ones (segment, region, cohort),
so that the identifying part can be anonymised while preserving the structure of the fact.
Alternative: pseudonymisation with a mapping table under strict control, or
crypto-shredding. A decision for
privacy-engineering-standards; the structure that makes it
possible is decided here, and if it is not decided here, later it is a migration.
- Facts do not carry PII. They carry keys. If a fact needs the name or the email, the
model is wrong.
- Granularity as a control: publishing the aggregated mart instead of the atomic fact is often the
simplest and most effective mitigation. A mart from which you cannot get to the person does not need
half the controls of one where you can.
- Row/column-level security applied in the consumption layer and modelled: the dimension
that determines visibility (organisation, territory) has to exist in the fact in order to
filter. If it is added later, it is a migration of the entire history.
- No real PII in the model's development environments (see
data-engineering-standards §4).
6. Model performance and operability
- Materialise by consumption, not by habit: the atomic fact materialised; intermediate layers
as views if they are queried rarely; aggregates and OBT materialised only with measured demand. Every
materialised table nobody queries is paid for on every run.
- Aggregates are derived and must be rebuildable from the atomic fact. An aggregate
that cannot be recomputed is an accidental source of truth.
- Sorting/clustering by the usual filter column (event date, and sometimes the
highest-cardinality filtering dimension). Physical partitioning and its cost belong to
data-engineering-standards; which column deserves to be the filter one is said by the model.
- Small wide dimensions, tall narrow facts: it is the split that columnar
engines reward. A dimension with 40 attributes and 50,000 rows is healthy; a fact with 40
descriptive columns is a modelling error.
- Watch the growth of SCD2s: a dimension historised on a volatile attribute grows
without bound and degrades every join. If it grows faster than the fact, the attribute was not
dimensional: it was a measure.
- Model health metrics, reviewed on a cadence: number of marts publishing the same
metric (target: one), models with no documented grain (target: zero), tests per model,
models with no consumption in the last quarter.
- An owner per conformed dimension and per metric, published. Without an owner, the
discrepancy between reports is resolved by nobody and becomes folklore.
7. Sustainability and prohibitions
- Schema evolution: adding is easy, changing the grain is a migration. Adding a
column or a dimension is additive and compatible. Changing a fact's grain, changing a
dimension from type 1 to type 2, or redefining a metric are not: they require rebuilding the
history, communicating with consumers and a coexistence period for the old and the
new version. Treat them as a migration with expand/contract, not as a code change.
- Redefining a metric is a communication event, not a commit. The published figures
will change retroactively; if nobody announces it, the business concludes the data is
unreliable. Version the metric and keep both for the duration of the transition.
- Active retirement: a mart, dimension or metric with no measured consumption for a quarter is flagged and
retired. The warehouse grows by accumulation by default.
- Mandatory ADR for: modelling paradigm, the grain of each central fact, historisation
policy per dimension, canonical time zone and fiscal calendar, and the decision to publish
an OBT.
- Review cadence: half-yearly for the status of the semantic layer specifications
(Apache Ossie, MetricFlow, Cube), which are actively moving.
FORBIDDEN
- ❌ Modelling without declaring the grain in writing and in the model itself. It is the root defect of the
domain.
- ❌ More than one grain in the same fact table.
- ❌ Publishing a table without a uniqueness test on its key: the grain is not proven.
- ❌ SCD type 2 without a demonstrated need, or applied to the whole dimension instead of to the
attributes that require it.
- ❌ Replacing an SCD type 2 with the table format's time travel.
- ❌ Marts that redefine metrics already defined elsewhere; or the same metric with the same
name and two definitions.
- ❌ Defining a metric inside a report or a BI tool.
- ❌ Storing ratios, percentages or averages as an aggregatable measure.
- ❌ Summing a semi-additive measure over time.
- ❌ A dimension with no "unknown" row, or an
INNER JOIN to a dimension that silently discards facts.
- ❌ A surrogate key exposed outside the warehouse or used as an integration key.
- ❌ Losing the source's natural key when modelling.
- ❌ Storing instants without a time zone, or lacking an explicit business
date_key.
- ❌ Reimplementing the fiscal calendar in every query instead of having it in the date dimension.
- ❌ OBT as the source of truth, or several OBTs of the same domain with different rules.
- ❌ Business logic in the consumption layer, or a consumption layer that skips the intermediate one.
- ❌ Data Vault without a written audit requirement.
- ❌ 3NF normalisation as the consumption layer for business users.
- ❌ PII in fact tables; dimensions that copy the entire source "just in case".
- ❌ Changing the grain, the type or the name of a published column without a migration and without warning.
- ❌ Trusting referential integrity "because the data comes in clean": in an analytical warehouse
there are no FKs, there are tests.
- ❌ Documenting the grain in a wiki instead of in the model.
- ❌ Pinning versions, licences or specification status from memory (§8).
8. Mandatory web verification
The data in §2 is from August 2026. Before committing anything to a deliverable, verify:
- Kimball reference: whether a 4th edition of The Data Warehouse Toolkit has appeared
(as of Aug 2026 the current one is the 3rd, from 2013, with no announced successor).
- Data Vault 2.0: the status of the standard and of its automation ecosystem; updated adoption
data before quoting percentages (those in §2.1 come from a commercially sponsored BARC study:
treat them as an order of magnitude, not as an exact figure).
- Semantic layer: the status of Apache Ossie (ASF incubator, formerly OSI; specification
v1.0 published in early 2026) and the real adoption of importers/exporters by
vendors — it is the only thing that turns the specification into portability. Licence and
activity of MetricFlow (Apache 2.0 since Oct 2025) and of Cube.
- dbt after the merger with Fivetran (completed 1 Jun 2026): which part of the semantic layer is
OSS and which part requires the paid platform; the metric serving API has been tied to the
commercial product (see
data-engineering-standards §2 and §8).
- Table format: schema evolution and time travel capabilities of Iceberg/Delta,
in case they shift the boundary with
lakehouse-standards (Iceberg 1.11.0 in May 2026).
- Functions and limits of the specific engine (BigQuery, Snowflake, Redshift, Databricks, Fabric) in whatever
the model takes for granted: clustering, materialised views, row-level security.
Declared gaps of this revision (do not fill in from memory):
- Apache Ossie: the number of vendors with an actually published importer/exporter has not been
verified; secondary sources suggest adoption lags far behind the announcement.
Do not promise metric portability without checking it product by product.
- Data Vault 2.0: Dan Linstedt's current position has not been verified, nor any revision of the
standard after 2025.
- Cube: version, current licence and current business model not verified in this revision.
- OBT versus star performance figures: the percentages in circulation come from a
vendor benchmark. Measure on your engine and with your data before using them to decide.
If the web contradicts this document, the web wins — flag the discrepancy.
1---2name: data-warehouse-modeling-standards3description: Use when deciding the shape of an analytical schema — star schema versus 3NF versus Data Vault (hubs, links, satellites, PIT and bridge tables), bronze/silver/gold or staging/intermediate/marts layering, declaring the grain of a fact table, additive versus semi-additive versus non-additive measures, factless facts, periodic and accumulating snapshots, surrogate versus natural keys, slowly changing dimensions (SCD type 0-7), conformed dimensions across marts, degenerate and junk dimensions, a date/calendar dimension with fiscal periods and business timezone versus UTC, One Big Table denormalization for a serving layer, the single definition of a metric and the semantic layer (MetricFlow, Cube, Apache Ossie/OSI semantic models), proving uniqueness and referential integrity on a model, or migrating a schema whose grain must change.4---56# Data warehouse modelling standards78Criteria verified as of **August 2026**. Re-verify on the web before committing to anything (§8).910## 1. Scope and triggers1112Applies to **deciding the shape of the analytical destination**: which tables exist, at what grain, with which13keys, how history is kept, how they are organised into layers and where the definition of a14metric lives.1516Triggers: `fct_`/`dim_`/`stg_`/`int_`, `schema.yml`/`semantic_models`/`metrics:`, `hub_`/17`lnk_`/`sat_`, `_sk`/`_key`/`surrogate`, `valid_from`/`valid_to`/`is_current`/`dbt_valid_to`,18`dim_date`/`dim_calendar`, `unique_key`, "star schema", "grano", "grain", "SCD2", "historise",19"conformed dimension", "OBT", "wide table", "marts", "gold layer", "medallion", "metric",20"semantic layer", and the questions that give away a badly declared model: **"why does revenue not21match between these two reports?"**, "how many rows should this table have?", "what does22a row represent here?", "why does the sum come out double?", "we want to see how the customer looked in23March".2425**Not applicable**: see26- `data-engineering-standards` (**sibling; boundary declared on both sides**): it **moves and27 transforms** —ingestion, ELT/ETL, orchestration, idempotency, *backfill*, Parquet, scan28 cost, freshness and data observability—; this one **decides the shape of the destination**. Cutting29 rule: if the question is "how do I reload March without duplicating?", it is theirs; if it is "what30 does a row represent and what happens when the customer changes segment?", it belongs here. A pipeline31 without a model produces a swamp; a model without a pipeline is a diagram.32- `data-platform-standards` (**parent**): **PostgreSQL as an operational engine** is theirs, with its33 normalised modelling, its indexes, its migrations and its partitioning. Here the **analytical34 destination** is modelled, which is something else and obeys other rules. Its guiding principle —**a warehouse by35 need, not by fashion**— is inherited: this skill does not authorise a new analytical warehouse; it assumes36 it was already justified. **A `dim_` is not created inside the transactional database.**37- `lakehouse-standards`: the **table format** —Iceberg, Delta, Hudi—,38 file-level schema evolution, hidden partitioning, *time travel* and engine snapshots. A fine and39 deliberate boundary: the table format's *time travel* gives you back **the table40 as it was**; an SCD type 2 tells you **how the business entity was**. They are not substitutes:41 the first is an infrastructure capability for recovery and technical audit, the second42 is a modelling decision that the business queries. **Never replace an SCD2 with *time43 travel*.**44- `analytics-bi-standards`: the BI tool, the dashboards, the45 governance of consumption and the performance of the presentation layer. **Explicit boundary46 decision: the canonical definition of a metric belongs here** (§3.7) —it lives in the repository,47 versioned, next to the model—; BI **consumes it**, it does not define it. A metric defined inside48 a report is a modelling defect, not a tooling choice.49- `data-governance-quality-standards`: catalogue, ownership, business50 glossary, contracts and quality as a programme. Here the **structural tests of the model** (§4).51- `streaming-cdc-standards`: change capture as a mechanism. Here only52 the use it is put to: feeding dimensional historisation.53- `microservices-architecture-standards`: **domain events, outbox and per-service data ownership54 are theirs**. A domain event is not an analytical fact: modelling the fact belongs here.55- `privacy-engineering-standards`: **retention, erasure, minimisation, pseudonymisation and56 personal data classification are theirs**; here they are **executed in the model** —which columns do not57 enter a dimension, how a subject is erased from an SCD2 without destroying the facts, where58 the mapping table lives—.59- `object-storage-standards` (S3 as the substrate), `observability-standards`, `mlops-standards`60 (**feature store and train/serve skew are theirs**; a *features* table is not a mart),61 `rag-standards`, `ai-governance-standards`, `grc-compliance-standards`, `bcdr-standards`,62 `backup-recovery-standards`, `cicd-standards`, `iac-standards`,63 `identity-access-management-standards`, `secrets-management-standards`,64 `aws-standards`/`azure-standards`/`gcp-standards` (**Redshift, BigQuery, Synapse/Fabric as65 managed services: cost, networking, IAM and provisioning are theirs; the modelling criteria66 on top belong here**), `api-design-standards`.67- Non-analytical engines: `nosql-standards`, `graph-db-standards`, `timeseries-db-standards`,68 `search-engines-standards`, `oracle-dba-standards`, `sqlserver-dba-standards`,69 `mysql-mariadb-dba-standards`.70- `sql-standards` (**the SQL language**, including the SQL contained in a dbt/SQLMesh model).71 Boundary mirrored from its §1: *"what does a row represent?"* —grain, facts, dimensions, SCD,72 layers, canonical metric definition— **belongs here**; *"how do I express that SCD2 in SQL without a73 correlated subquery?"* is theirs. **SQL living inside a model does not exempt it from74 the language criteria.**7576**Domain thesis — modelling still matters**. The argument that cheap storage77and elastic compute retired modelling confuses the cost that disappeared with the ones that remain.78Disk stopped mattering; **compute, comprehension and trust did not**:7980- **Compute**: in modern engines you pay per scan. A table with no declared grain is81 queried badly and paid for every time.82- **Comprehension**: if nobody knows what a row represents, every analyst invents their interpretation and83 all of them are plausible.84- **Trust**: it is the scarce resource. **Two reports that disagree destroy trust in the85 platform faster than an error that is detected and fixed** — an error gets fixed; the86 discrepancy teaches the organisation that data is a matter of opinion.8788What has changed is the **implementation**: fewer physical star schemas built by hand, more89tool-managed models and semantic layers on top. The obligations that survive are90the same: **declare the grain, separate fact from dimension, conform dimensions and historise what91changes**.9293## 2. Default decisions9495> Verify on the web the status of the references, specifications and tools before pinning them96> in a real project (§8).9798### 2.1 Choice of paradigm99100| Paradigm | When it is the right answer | Real cost | Verdict |101|---|---|---|---|102| **Kimball / dimensional (star)** | **Default.** The destination is queried to answer business questions | Requires declaring the grain and conforming dimensions — intellectual work, not technical | **Adopt this unless there is a written reason** |103| **Inmon / corporate 3NF** | Integration of many systems with complex business rules and a central team that sustains it, feeding dimensional marts downstream | Slow to deliver; incomprehensible for the end consumer: **it is never the consumption layer** | Intermediate layer in large organisations |104| **Data Vault 2.0** (hubs, links, satellites) | Many changing sources **and** a requirement for full auditability: who said what, when and from which system (banking, insurance, regulated sector) | **Table explosion** (of the order of 3× against the equivalent relational model) and unmanageable *joins*; it forces *PIT* and *bridge* tables just to be able to query; and building dimensional marts on top **anyway** | **It solves an audit and integration problem most people do not have.** The adoption data confirms it: adopters themselves cite as the main drawbacks training needs (~48 %), implementation complexity (~35 %) and query performance (~32 %), and only ~31 % say their implementation fully meets the standard. It requires automation and trained staff |105| **One Big Table (OBT)** | **Serving layer**, derived from a model, for a dashboard or a specific case (§3.6) | It is rebuilt in full; it specialises and multiplies | Correct as a destination, **never as the source of truth** |106107Rule: **Data Vault without an explicit audit requirement is over-engineering**, and specifically the108worst kind of over-engineering: the kind that imposes a permanent explanation tax on everyone who109joins the team.110111### 2.2 Modelling toolchain112113| Area | Default | Note (Aug 2026) |114|---|---|---|115| Canonical dimensional reference | **Kimball & Ross, *The Data Warehouse Toolkit*, 3rd ed. (2013)** | **There is no 4th edition**; the Kimball Group ceased its consulting/training activity in 2016. It is still the reference, with the caveat that its ETL chapters describe a pre-ELT era |116| Metric definition | **In the repository, in versioned YAML, next to the model** | **MetricFlow** relicensed to **Apache 2.0** (Oct 2025). **Cube** as a *headless* alternative (it defines **and serves**: they are not interchangeable) |117| Semantic interchange specification | **Apache Ossie (incubating)**, formerly *Open Semantic Interchange* (OSI) | Apache-2.0, JSON/YAML specification. It is a **specification**, not a product: it stores nothing and serves nothing. Watch the real adoption of importers before betting on portability |118| Model tests | Versioned declarative assertions (uniqueness, not null, relationships, accepted values) | Execution gate (§4) |119| Grain documentation | In the model itself, not in a wiki | A grain documented outside the code stops being true within a quarter |120121## 3. Structure and conventions122123### 3.1 Layers124125Three, with separated responsibilities. The names vary (bronze/silver/gold,126*staging*/intermediate/marts); the rules do not:127128| Layer | Content | Hard rule |129|---|---|---|130| **Raw / bronze** | Faithful copy of the source, immutable | It is not modelled. It belongs to `data-engineering-standards` |131| **Intermediate / silver** | Typing, deduplication, entity conformance, business rules | This is where the ugly logic lives, and **only here** |132| **Consumption / gold / marts** | Facts and dimensions, or the OBT derived from them | **It must be boring**: stable names, stable types, zero surprises |133134That the consumption layer is boring is a requirement, not a style. It is the layer people read without135asking you, the one that breaks reports when it changes and the one that sets the contract with the business. Every136surprise (a column that is sometimes null, a `status` with new values and no warning, a metric137whose definition changed silently) is paid for in trust, and trust is not recovered with a138*hotfix*.139140**Forbidden to skip the intermediate layer** by putting business logic in the consumption layer: the141result is marts that contradict each other because each one reimplemented the same rule slightly142differently.143144### 3.2 Facts: the grain is the decision145146**The grain is the most important decision and the one most often got wrong.** It is declared **first**, in a147natural-language sentence, before writing a single column:148149> "One row = **one line of an order** at the moment it was confirmed."150151Rules:152- **The grain is written in the model** (the table's description), not in anybody's head.153- **Atomic grain by default**: the finest the source allows. Aggregating afterwards is trivial;154 disaggregating is impossible. Pre-aggregates are derived optimisations, not the fact.155- **One grain per table.** Mixing order lines and order headers in the same table is the156 root cause of half of the industry's inflated figures: any sum multiplies by the157 number of lines.158- **The grain is tested, not believed** (§4): if the declared key is not unique, your grain is not what you159 think it is, and everything built on top is wrong.160161**Measure types** — the classification decides which aggregations are legal, and it must be declared:162163| Type | Sums over any dimension | Example | Trap |164|---|---|---|---|165| **Additive** | Yes | Sales amount | — |166| **Semi-additive** | Yes, except over time | Account balance, inventory level | Summing 12 months of balances gives a meaningless figure. Over time: last value or average |167| **Non-additive** | No | Ratios, percentages, unit margins | **Never store the ratio**: store the numerator and denominator and divide when aggregating. Summing ratios is always incorrect |168169**Fact table types**:170- **Transactional**: one row per event, atomic grain. The default.171- **Periodic snapshot**: one row per entity and period (daily balance, weekly inventory). For172 semi-additive measures and for "how much was there" questions. It grows predictably.173- **Accumulating snapshot**: one row per process with multiple milestones, **which is updated** as it174 progresses (ordered → paid → shipped → delivered). To measure latencies between phases. It is the only175 fact table that gets updated, and it must be stated explicitly because it breaks the expectation176 of immutability.177- **Factless**: it records that something happened or that a relationship exists, with no measure.178 Attendance, coverage, eligibility. Legitimate and heavily under-used: the usual alternative is179 inventing a `count = 1` column, which is the same thing with a worse name.180181In addition: **degenerate dimensions** (order/ticket number) live in the fact, with no table of their own;182**junk dimensions** group low-cardinality flags that do not deserve one dimension183each.184185### 3.3 Dimensions186187- **Surrogate keys** in historised dimensions (SCD2) — they are **mandatory** there, because188 the natural key stops being unique once there are several versions. In non-historised dimensions, the189 natural key is acceptable and simpler; a conscious decision, not an automatism.190- **The natural key never disappears from the model**: it is kept as a column, because it is the only thing191 that allows reconciling with the source system when someone asks.192- **Never use the surrogate key as an integration key between systems** nor expose it to the193 outside: it is an internal detail of the warehouse.194- **Every dimension has an "unknown"/"not applicable" row** with a fixed key (typically `-1`).195 Without it, facts with a missing dimension are lost in the `INNER JOIN` and the figures drop without196 anyone knowing why. This is a `LEFT JOIN` that silently unbalances the total.197- **Hierarchies**: inside the dimension itself (country → region → city) as long as they are fixed.198 Ragged or variable-depth hierarchies require a *bridge* table — and they are the199 most frequent excuse for complicating a model that did not need it.200201**Slowly changing dimensions (SCD)** — Kimball defines types 0 to 7; in practice202only three are used, and the choice is made by business question, not by habit:203204| Type | Behaviour | When |205|---|---|---|206| **0** | The attribute never changes | Original sign-up date, country of birth |207| **1** | Overwrite | **The default.** The business does not need the previous value (fixing a typo, current phone number) |208| **2** | New row with `valid_from`/`valid_to`/`is_current` | **When the business needs to analyse the past with the values of that time**: change of customer segment, of sales territory, of tariff |209| **3** | A "previous value" column | A real niche: a one-off reorganisation where you must report by the old structure **and** the new one at the same time. It is not a general alternative to type 2 |210211Decision criterion, in one question: **"when this attribute changes, should historical reports212reflect the value of that time or the current one?"** If it is "of that time", type 2. If it is "the current213one", type 1. If nobody can answer, it is type 1 — and the fact that it was asked is documented.214215**SCD type 2 is not free** and that is why it is in the prohibitions without further need: it multiplies rows,216forces **every** *join* from the fact to use the surrogate key valid at the event's date217(not the current one), breaks naive counts of the dimension (`COUNT(*)` stops being "number of218customers") and complicates erasure under the right to be forgotten. Apply it to the attributes that require it,219**not to the whole dimension**.220221**Conformed dimensions** — the mechanism that makes two marts comparable. A conformed222dimension is the same dimension (same keys, same attributes, same meaning) used by223several facts. Without them, "sales by region" and "returns by region" cannot be crossed224because "region" does not mean the same thing, and nobody discovers it until the figures do not match in a225meeting. Rule: **one business entity = one dimension = one owner.** If a mart needs226a variant, it is justified in writing or the dimension is conformed.227228### 3.4 The date dimension229230A physical table, populated in advance, with one row per day (and another per hour/minute if needed,231**separate**). It is not optional and it is not replaced by the engine's date functions: it exists to232hold what the engine does not know —holidays, fiscal calendar, working weeks, seasons— and so233that queries filter by attribute instead of by expression.234235- Key in `YYYYMMDD` format (integer) or a date: readable in the fact, sortable, no *join* to236 debug.237- **Fiscal calendar** as its own columns (`fiscal_year`, `fiscal_quarter`, `fiscal_period`):238 the fiscal year almost never matches the calendar one, and reimplementing it in every query guarantees239 that some will match and others will not.240241### 3.5 The date problem — local time versus UTC242243A recurring source of discrepancies between reports that nobody can explain. A rule in three parts,244all mandatory:2452461. **Always store the instant in UTC** with a zoned type (`timestamptz`/`TIMESTAMP` with zone).247 Never a type without a zone: "2 pm" with no zone is an incomplete datum disguised as a complete one.2482. **Also store the business date key** (`date_key`) already resolved in the **business249 time zone**, decided and documented. It is the column you filter and group by. Without it,250 every query converts in its own way and "yesterday's sales" gives three different figures depending on who251 asks.2523. **Document what the business zone is and what happens with multi-country operations**: either a single253 canonical zone, or a local `date_key` **per country** in addition to the canonical one — chosen254 explicitly and written in the model.255256Additions: store the *offset* if the daylight-saving change matters (23- and 25-hour257days exist and break annual reconciliations); and decide which date rules —event date, posting258date, ingestion date— per fact. **The three are different and all three are necessary**; the259one used to partition is not necessarily the one used to report.260261### 3.6 Deliberate denormalisation: OBT262263Flattening everything into a wide table is **correct** when: (a) it serves a specific consumption case,264(b) it **is derived** from a dimensional model that still exists, (c) its rebuild is265automatic, and (d) it is documented as a serving view.266267The gain is real and measurable —columnar MPP engines favour the wide table over the *join*268at query time—, but it is a **last-mile** gain.269270It is **laziness** when: it replaces the model instead of being derived from it, every team has its own with271different rules, nobody knows what grain it is at, or the metric is defined inside the table itself and272gets duplicated in the next one. Unmistakable symptom: **there are three wide "sales" tables and none273matches the others**.274275### 3.7 Metrics and the semantic layer276277**The definition of "revenue" or of "active customer" is a repository artifact, versioned,278reviewed by *pull request* and with an owner.** It does not live in a report, nor in a spreadsheet,279nor in the head of whoever has been there longest.280281- **One definition, one name.** If the business has two notions of revenue (gross and net of282 returns), they are **two metrics with two names**, not one ambiguous metric.283- **The metric is defined over the dimensional model**, declaring the measure, the aggregation and the284 dimensions it is legal to break it down by. A metric with no declared dimensions will be285 broken down by something that makes no sense.286- **The semantic layer does not fix a bad model**: if the grain is wrong, the metric computed287 on top is wrong with more steps and better presentation.288- **Portability, with measured scepticism**: Apache Ossie (incubating) is the interchange289 specification, and MetricFlow (Apache 2.0) its reference declarative format. The specification290 does not carry the serving architecture —cache, pre-aggregates, multi-tenancy, APIs—: that is not291 portable even if the definition is. Adopt the specification out of hygiene; do not assume it as292 a guarantee of exit.293- **A metric with no owner = a dead metric.** When two reports disagree, there has to be294 someone who decides which is correct, and within how long.295296## 4. Quality and testing — gates297298A model's tests are not a matter of opinion: they are what proves the model is what you say299it is. In increasing order of cost; **all of them are gates on publishing the consumption layer**300unless stated otherwise.3013021. **Uniqueness of the grain key.** The test that proves the declared grain is real. If303 the key is composite, the combination is tested. **Without this test there is no model, there is a table.**3042. **Not null** on keys, on dimension foreign keys and on the columns a metric305 depends on.3063. **Referential integrity**: every dimension key in the fact exists in the dimension. Analytical307 warehouses do not enforce FKs; therefore it is tested, or it does not exist.3084. **Accepted values** on catalogue columns (`status`, `type`, `channel`): it catches the new309 value the source introduced without warning **before** it shows up as "other" in a report.3105. **Ranges and signs**: amounts that cannot be negative, dates that cannot be in the311 future, bounded quantities.3126. **SCD2 integrity** — systematically forgotten and the place where things break most silently:313 - Exactly **one** current row (`is_current`) per natural key.314 - The `[valid_from, valid_to)` intervals of the same natural key **do not overlap and leave no315 gaps**.316 - The first version starts at the beginning of time and the current one has no end (or has a317 documented sentinel date, always the same one).3187. **Reconciliation with the source**: row count and sum of a key measure against the source system,319 with a declared tolerance. It is the only test that detects row loss upstream.3208. **Reconciliation between layers and between marts**: the canonical metric computed from the atomic fact and321 from the aggregate/OBT must match. **If two marts publish the same metric, a test compares322 their results** — this is the test that prevents the domain's most expensive failure.3239. **Non-multiplication (*fan-out*) test**: after the model's *joins*, the fact's row324 count does not change. It catches the *join* with a dimension that was not unique, which is the325 mechanical cause of inflated figures.32610. **Unit tests of the business logic** with fixed input rows and expected output,327 covering edges: nulls, an SCD2 version change exactly at the instant of the event, a row arriving328 out of order, an entity with no dimension, a zero amount, a full refund.32911. **Consumption-layer contract**: removing or renaming a published column, or changing the330 type, **breaks the build** unless there is explicit recorded approval. The consumption layer is an API.33112. **Documentation as a gate**: a model with no grain description and no documented columns does not332 merge.333334## 5. Security and personal data in the model335336- **Classify while modelling, not afterwards.** Every column of a dimension knows whether it is public, internal,337 confidential or personal. The classification decides who sees it and how long it lives338 (see `privacy-engineering-standards`).339- **Minimisation in the model**: a dimension is not a dumping ground for every field in the source.340 Every copied attribute is an attribute you have to retain, erase and explain in a breach. Copying341 the whole `customer` table "just in case" is a privacy decision taken by omission.342- **The right to be forgotten and SCD2 collide head-on**: erasing a subject from a historised343 dimension destroys the traceability of the facts if it is done naively. Design from the344 start: **identifying attributes separated** from the analytical ones (segment, region, cohort),345 so that the identifying part can be anonymised while preserving the structure of the fact.346 Alternative: pseudonymisation with a mapping table under strict control, or347 *crypto-shredding*. A decision for `privacy-engineering-standards`; **the structure that makes it348 possible is decided here, and if it is not decided here, later it is a migration**.349- **Facts do not carry PII.** They carry keys. If a fact needs the name or the email, the350 model is wrong.351- **Granularity as a control**: publishing the aggregated mart instead of the atomic fact is often the352 simplest and most effective mitigation. A mart from which you cannot get to the person does not need353 half the controls of one where you can.354- **Row/column-level security** applied in the consumption layer and **modelled**: the dimension355 that determines visibility (organisation, territory) has to exist in the fact in order to356 filter. If it is added later, it is a migration of the entire history.357- No real PII in the model's development environments (see `data-engineering-standards` §4).358359## 6. Model performance and operability360361- **Materialise by consumption, not by habit**: the atomic fact materialised; intermediate layers362 as views if they are queried rarely; aggregates and OBT materialised only with measured demand. Every363 materialised table nobody queries is paid for on every run.364- **Aggregates are derived and must be rebuildable** from the atomic fact. An aggregate365 that cannot be recomputed is an accidental source of truth.366- **Sorting/clustering by the usual filter column** (event date, and sometimes the367 highest-cardinality filtering dimension). Physical partitioning and its cost belong to368 `data-engineering-standards`; **which column deserves to be the filter one is said by the model**.369- **Small wide dimensions, tall narrow facts**: it is the split that columnar370 engines reward. A dimension with 40 attributes and 50,000 rows is healthy; a fact with 40371 descriptive columns is a modelling error.372- **Watch the growth of SCD2s**: a dimension historised on a volatile attribute grows373 without bound and degrades every *join*. If it grows faster than the fact, the attribute was not374 dimensional: it was a measure.375- **Model health metrics**, reviewed on a cadence: number of marts publishing the same376 metric (target: one), models with no documented grain (target: zero), tests per model,377 models with no consumption in the last quarter.378- **An owner per conformed dimension and per metric**, published. Without an owner, the379 discrepancy between reports is resolved by nobody and becomes folklore.380381## 7. Sustainability and prohibitions382383- **Schema evolution**: **adding is easy, changing the grain is a migration.** Adding a384 column or a dimension is additive and compatible. Changing a fact's grain, changing a385 dimension from type 1 to type 2, or redefining a metric **are not**: they require rebuilding the386 history, communicating with consumers and a coexistence period for the old and the387 new version. Treat them as a migration with expand/contract, not as a code change.388- **Redefining a metric is a communication event, not a commit.** The published figures389 will change retroactively; if nobody announces it, the business concludes the data is390 unreliable. Version the metric and keep both for the duration of the transition.391- **Active retirement**: a mart, dimension or metric with no measured consumption for a quarter is flagged and392 retired. The warehouse grows by accumulation by default.393- **Mandatory ADR** for: modelling paradigm, the grain of each central fact, historisation394 policy per dimension, canonical time zone and fiscal calendar, and the decision to publish395 an OBT.396- **Review cadence**: half-yearly for the status of the semantic layer specifications397 (Apache Ossie, MetricFlow, Cube), which are actively moving.398399**FORBIDDEN**400- ❌ **Modelling without declaring the grain** in writing and in the model itself. It is the root defect of the401 domain.402- ❌ More than one grain in the same fact table.403- ❌ Publishing a table without a uniqueness test on its key: the grain is not proven.404- ❌ **SCD type 2 without a demonstrated need**, or applied to the whole dimension instead of to the405 attributes that require it.406- ❌ Replacing an SCD type 2 with the table format's *time travel*.407- ❌ **Marts that redefine metrics** already defined elsewhere; or the same metric with the same408 name and two definitions.409- ❌ Defining a metric inside a report or a BI tool.410- ❌ Storing ratios, percentages or averages as an aggregatable measure.411- ❌ Summing a semi-additive measure over time.412- ❌ A dimension with no "unknown" row, or an `INNER JOIN` to a dimension that silently discards facts.413- ❌ A surrogate key exposed outside the warehouse or used as an integration key.414- ❌ Losing the source's natural key when modelling.415- ❌ Storing instants without a time zone, or lacking an explicit business `date_key`.416- ❌ Reimplementing the fiscal calendar in every query instead of having it in the date dimension.417- ❌ OBT as the source of truth, or several OBTs of the same domain with different rules.418- ❌ Business logic in the consumption layer, or a consumption layer that skips the intermediate one.419- ❌ Data Vault without a written audit requirement.420- ❌ 3NF normalisation as the consumption layer for business users.421- ❌ PII in fact tables; dimensions that copy the entire source "just in case".422- ❌ Changing the grain, the type or the name of a published column without a migration and without warning.423- ❌ Trusting referential integrity "because the data comes in clean": in an analytical warehouse424 there are no FKs, there are tests.425- ❌ Documenting the grain in a wiki instead of in the model.426- ❌ Pinning versions, licences or specification status from memory (§8).427428## 8. Mandatory web verification429430The data in §2 is from **August 2026**. Before committing anything to a deliverable, verify:4314321. **Kimball reference**: whether a 4th edition of *The Data Warehouse Toolkit* has appeared433 (as of Aug 2026 the current one is the **3rd, from 2013**, with no announced successor).4342. **Data Vault 2.0**: the status of the standard and of its automation ecosystem; updated adoption435 data before quoting percentages (those in §2.1 come from a commercially sponsored BARC study:436 **treat them as an order of magnitude, not as an exact figure**).4373. **Semantic layer**: the status of **Apache Ossie** (ASF incubator, formerly OSI; specification438 v1.0 published in early 2026) and **the real adoption of importers/exporters by439 vendors** — it is the only thing that turns the specification into portability. Licence and440 activity of **MetricFlow** (Apache 2.0 since Oct 2025) and of **Cube**.4414. **dbt after the merger with Fivetran** (completed 1 Jun 2026): which part of the semantic layer is442 OSS and which part requires the paid platform; the metric serving API has been tied to the443 commercial product (see `data-engineering-standards` §2 and §8).4445. **Table format**: schema evolution and *time travel* capabilities of Iceberg/Delta,445 in case they shift the boundary with `lakehouse-standards` (Iceberg 1.11.0 in May 2026).4466. Functions and limits of the specific engine (BigQuery, Snowflake, Redshift, Databricks, Fabric) in whatever447 the model takes for granted: *clustering*, materialised views, row-level security.448449**Declared gaps of this revision** (do not fill in from memory):450- **Apache Ossie**: the number of vendors with an **actually published** importer/exporter has not been451 verified; secondary sources suggest adoption lags far behind the announcement.452 Do not promise metric portability without checking it product by product.453- **Data Vault 2.0**: Dan Linstedt's current position has not been verified, nor any revision of the454 standard after 2025.455- **Cube**: version, current licence and current business model not verified in this revision.456- **OBT versus star performance figures**: the percentages in circulation come from a457 vendor *benchmark*. Measure on your engine and with your data before using them to decide.458459If the web contradicts this document, **the web wins** — flag the discrepancy.