Beyond Kimball: Does Any of This Still Matter?
Inmon, Data Vault, One Big Table, activity schemas, and the lakehouse walk into a columnar warehouse — and the honest verdict on whether star schemas still earn their place.
Kimball wrote The Data Warehouse Toolkit in 1996, when a join was expensive, storage was precious, and a nightly batch that finished before the analysts arrived was a genuine engineering achievement. Sixteen posts into building star schemas, it’s fair to ask the uncomfortable question: on a warehouse that shrugs off a billion-row join, with storage that costs less than the meeting to discuss it, does any of this discipline still pay for itself?
That’s the right question to end on, and it deserves a real answer rather than a loyal one. To get there, you have to know what Kimball was always competing with — because dimensional modeling was never the only way to build a warehouse, and the alternatives each make a case worth hearing. Some of them are older than Kimball’s front-end popularity; some were born on the columnar warehouses that made this series’ whole toolchain possible. We’ll build the two that matter most — One Big Table and Data Vault — in dbt against the bookshop you already know, so the tradeoffs are concrete rather than folklore, and then look at the newer event-shaped and lakehouse-shaped answers before rendering a verdict.
Inmon: build the foundation first
Bill Inmon, often called the father of the data warehouse, argued the opposite of Kimball from the start. His model is top-down: build a single, normalized, enterprise-wide warehouse in third normal form — the integrated, non-redundant source of truth for the whole business — and then spin dimensional data marts off it for each department to query.
The appeal is integration. Every entity is defined once, reconciled across every source system, before anyone builds a report. Finance and marketing can’t disagree about what a “customer” is because there’s exactly one normalized customer table upstream of both. The cost is time and patience: you’re modeling the entire enterprise before you ship a dashboard, and 3NF is not what a BI tool wants to query. Inmon-versus-Kimball was the warehouse-design holy war for two decades.
In practice most teams landed on a blend — an integrated, normalized core feeding dimensional marts — which, notice, is Inmon’s architecture with Kimball’s front end. The marts are still stars. That blend is worth holding in mind, because it’s exactly the shape the decision matrix at the end of this post keeps returning to: a governed core in one physical style, a presentation layer in another. Inmon’s contribution to the modern stack isn’t a table you’ll build; it’s the insistence that conformance happens somewhere upstream of the dashboard, which is the same instinct the bus matrix and conformed dimensions encode.
One Big Table: just flatten it
The alternative you’re most likely to actually meet in a modern stack is the least academic. One Big Table — OBT, or “wide table” modeling — throws out the star at the presentation layer. Instead of a fact joined to a dozen dimensions, you denormalize the whole thing into a single very wide table: every order line with its customer attributes, book attributes, date attributes, and flags all flattened onto one row. No joins at query time. The BI tool selects from one table and flies.
This exists because the economics flipped. Columnar warehouses like Snowflake, BigQuery, and Redshift only read the columns a query touches, store data cheaply, and compress redundant values well — so the two things that made denormalization a sin in 1996, storage cost and read cost, mostly evaporated. dbt made the rebuild-on-demand cheap enough that the redundancy stopped being scary: your wide table is just another model, regenerated every run.
Here’s the part the OBT enthusiasts sometimes skip, and it’s the whole thesis of this section: a good OBT is a star schema that got flattened at the last step, not a star schema that never got built. You design dimensionally — declare the grain, conform the dimensions, sort out additivity — and then you denormalize. Watch how the build makes that literal.
-- models/marts/obt_sales.sql
{{ config(materialized='table') }}
-- grain: one row per order item — inherited unchanged from fct_order_items
select
f.order_item_sk,
f.order_id, -- degenerate dimension, carried through
-- date attributes, flattened out of dim_date
d.date_day,
d.calendar_year,
d.calendar_month,
d.day_of_week,
d.is_weekend,
-- customer attributes, flattened out of dim_customer
c.customer_id,
c.customer_name,
c.segment,
c.country,
-- book attributes, flattened out of dim_book
b.book_id,
b.title,
b.author,
b.genre,
b.list_price,
-- measures, straight off the fact
f.quantity,
f.extended_price,
f.margin_amount
from {{ ref('fct_order_items') }} as f
join {{ ref('dim_date') }} as d using (date_sk)
join {{ ref('dim_customer') }} as c using (customer_sk)
join {{ ref('dim_book') }} as b using (book_sk)
Notice what this model is. It’s fct_order_items with its three dimensions pre-joined and the descriptive columns spread out onto every row. The grain is still one order line — the OBT inherits it from the fact and doesn’t invent a new one. The measures are the same additive quantity, extended_price, and margin_amount you were careful about back in the fact-table post; there is still, deliberately, no margin_pct column, because flattening the dimensions onto the row doesn’t make a ratio safe to average. The whole star is still in there. It’s just been pressed flat.
That framing also tells you exactly where the footguns are, because they’re all consequences of the flattening — not of the star underneath it.
The grain is baked in, and only one grain fits. The star let dim_date serve fct_order_items, an inventory snapshot, and a returns fact all at once. obt_sales serves questions at the order-line grain and nothing else. A question the shape didn’t anticipate — “sessions per customer,” “on-hand stock by day” — isn’t a different GROUP BY; it’s a different wide table you have to design and build. You’ve traded one flexible model for several rigid ones.
Additivity gets easier to get wrong, not harder. In a star, nobody accidentally sums a customer attribute — it lives in the dimension, one row per customer. In obt_sales, country and segment are repeated on every order line that customer ever placed, so a naive count(*) where segment = 'trade' counts order lines, not customers, and count(distinct customer_id) is now mandatory where a dimension query would have been obvious. The grain still governs additivity; the flattening just hides the grain from anyone reading the table.
SCD becomes a live wire. This is the subtle one, and it’s where a flattened model quietly loses something a star keeps.
Recall the design this book actually chose (chapter 09): customer_sk is a durable key — generate_surrogate_key(['customer_id']) — which is identical across every version of a customer. That was a deliberate call, and it means the fact does not carry a pointer to “the customer as they were at order time.” It carries a pointer to the customer, full stop. The version is recovered at query time, by a temporal join against valid_from/valid_to.
Which is exactly what the OBT throws away. Flatten dim_customer — the current-only view — onto every order line, and each line now carries today’s segment, not the segment that was true when the order was placed. Your 2024 revenue silently re-attributes itself every time a customer changes tier. Nobody gets an error; the numbers just quietly stop being reproducible, and last quarter’s board deck no longer regenerates the numbers that were in last quarter’s board deck.
The star didn’t protect you by magic either — a naive join to the current-only view has the same problem. What the star gives you is the option: the versioned dimension is still sitting there, and the temporal join is available when you need it. The OBT has already collapsed the choice. So if you flatten a Type 2 dimension, you must decide, explicitly and in writing, which of two tables you are building: a point-in-time OBT (join dim_customer_history temporally, carry the as-of attributes, and the table is a historical record) or a current-state OBT (carry today’s attributes, and the table is a snapshot that must never be used for period-over-period comparison). Both are legitimate. Shipping one while your stakeholders assume the other is how a dashboard becomes a liability.
Storage and re-computation are real, just cheap. Every customer attribute is repeated on every one of their order lines; every book attribute on every line that sold it. Columnar compression eats most of that redundancy, so the storage bill is smaller than it looks — but materialized='table' here means a full rebuild of the whole wide table every run. On the bookshop’s volumes that’s nothing; on a billion-line fact it’s a reason to switch to materialized='incremental' and rebuild only the recent grain, exactly as you would for the fact itself.
None of these are arguments against OBT. They’re arguments that OBT is a materialization of a dimensional design, and that you pay for skipping the design in exactly the currencies above. Build the star’s thinking, ship the wide table.
The semantic layer: the other answer to rigidity
OBT solves query speed and simplicity by freezing one shape. The semantic layer solves the same “make the star easy to query” problem from the opposite direction — instead of flattening the model into a table, it puts a governed layer above the star that knows how to assemble the join and compute the metric on demand.
The trade is the mirror image of OBT’s. Where a wide table pre-joins everything and locks the grain, a semantic layer keeps the normalized star and generates the right query per question — so gross_sales is defined once, and asking for it by month, by genre, or by customer segment is three different generated queries against the same conformed dimensions, no new table required. You get OBT’s single-definition consistency without OBT’s single-grain rigidity, at the cost of a query-time join the wide table paid for at build time. Many production stacks run both: wide tables for the handful of hot, known dashboards, and a semantic layer over the star for everything open-ended. That whole post is on the semantic layer; the point here is only that it and OBT are two answers to the same complaint, and that both of them still require the underlying dimensional model to have a declared grain and conformed dimensions to be correct.
Data Vault: model for audit and change
Data Vault, from Dan Linstedt, answers a question the other approaches mostly ignore: how do you load an enterprise warehouse that changes constantly, from dozens of sources, without rework every time one of them shifts? Its answer is to decompose everything into three insert-only building blocks — hubs (the business keys), links (the relationships between them), and satellites (the descriptive attributes and their history). Nothing is ever updated; new facts arrive as new rows.
Sketch it against the bookshop and the shape gets concrete. A hub is just the distinct business keys, hashed, with load metadata — no attributes, no relationships:
-- hub_customer.sql — one row per customer business key, insert-only
select distinct
{{ dbt_utils.generate_surrogate_key(['customer_id']) }} as customer_hk,
customer_id, -- the natural business key
current_timestamp() as load_ts,
'bookshop_oltp' as record_source
from {{ ref('stg_customers') }}
-- hub_book.sql is the same shape, keyed on book_id -> book_hk
A link carries the relationships — an order connecting a customer to a book — and again holds nothing descriptive, only the hash keys it ties together:
-- link_order.sql — one row per (order, customer, book) association
select distinct
{{ dbt_utils.generate_surrogate_key(['oi.order_id', 'o.customer_id', 'oi.book_id']) }} as order_hk,
{{ dbt_utils.generate_surrogate_key(['o.customer_id']) }} as customer_hk,
{{ dbt_utils.generate_surrogate_key(['oi.book_id']) }} as book_hk,
oi.order_id,
current_timestamp() as load_ts,
'bookshop_oltp' as record_source
from {{ ref('stg_order_items') }} as oi
join {{ ref('stg_orders') }} as o using (order_id)
A satellite hangs the descriptive attributes off a hub, historized — one row per change, never updated, tagged with a hashdiff so a load can tell whether anything actually changed:
-- sat_customer.sql — descriptive history for a customer, insert-only
select
{{ dbt_utils.generate_surrogate_key(['customer_id']) }} as customer_hk,
{{ dbt_utils.generate_surrogate_key(
['customer_name', 'segment', 'country']) }} as hashdiff,
customer_name,
segment,
country,
current_timestamp() as load_ts,
'bookshop_oltp' as record_source
from {{ ref('stg_customers') }}
Read those four models and the promise is obvious. Loading is relentlessly agile: a new source system is new satellites, not a schema migration; the customer’s attributes changing is a new sat_customer row, not an UPDATE; the whole thing is insert-only and therefore perfectly auditable — you can reconstruct exactly what the warehouse knew at any past load. This is genuinely the right tool for a large, regulated, multi-source warehouse where ingestion churn and audit are the dominant problems.
Now try to answer a business question from it: current name and segment for every customer who bought a given book. You join hub_book to link_order to hub_customer to sat_customer, and because the satellite is insert-only history you also have to filter each hub’s satellite down to its latest load_ts — a window function per satellite, on top of a chain of hash-key joins no BI tool was ever built to write. That’s the trade Data Vault makes on purpose: flexible to load, a nightmare to query. It doesn’t replace Kimball; it sits underneath it. You build dimensional marts — stars, or OBTs flattened from stars — on top of the Vault for anyone to actually query, and the Vault hands those marts clean, historized, auditable inputs. The sat_customer history above is, in fact, exactly the raw material a Type 2 dim_customer is built from.
Activity schema and the event stream
The event-oriented shops model differently again. An activity schema collapses the whole warehouse toward a single, very tall event stream: one table, roughly (entity, activity, timestamp, attributes) — customer 4102 placed an order at this time, viewed a book at that time, returned an item later. Instead of many facts at many grains, you have one uniform stream of things-that-happened, and analysis is done by relating activities to each other in time (“the order that followed a search within ten minutes”), typically through a specialized set of temporal join patterns.
The appeal is uniformity and sequence. Funnels, retention, time-to-next-event, and behavioral cohorts fall out naturally because the model is about sequence, where a star has to reconstruct sequence by joining facts. The cost is that ordinary aggregate reporting — revenue by genre by month — gets harder, not easier: you’re deriving measures and dimensions back out of a generic attribute bag at query time, which is precisely the work a fact table did once, at load. Activity schemas shine for product-analytics and behavioral questions and struggle at exactly the financial roll-ups stars were built for, which is why in practice they tend to feed dimensional marts rather than replace them — the stream is the source, the star is the report.
Wide events and the lakehouse
The lakehouse shifts the ground under all of this. On Iceberg or Delta tables, the “warehouse” is open columnar files with a table format over them, and the reigning pattern is medallion: a bronze layer of raw ingested data, a silver layer of cleaned and conformed entities, and a gold layer of business-facing aggregates. Read those three layers honestly and they are Inmon’s argument in new clothing — bronze/silver is a normalized, conformed core; gold is the presentation layer — and the gold layer, overwhelmingly, is built as stars or OBTs. Medallion tells you where transformation happens; it stays silent on the shape of the final tables, and dimensional modeling is what fills that silence.
Alongside it runs the wide-event movement, loudest in observability: don’t pre-aggregate into narrow metrics, emit one enormously wide event per unit of work — hundreds of columns, every attribute of the request captured — and slice it at query time. It’s OBT’s philosophy pushed to its limit (denormalize everything, decide the questions later) and it thrives for the same reason on the same columnar engines. But it inherits OBT’s exact liability: a wide event is still at a grain, its repeated attributes still fan out if you count them naively, and someone still has to know what one row means. The technology is new. The discipline it needs is the one this series has been teaching.
Choosing a shape: a decision matrix
Put the physical shapes side by side against the things that actually decide between them — the workload, the team, and the warehouse:
| Shape | Best when… | Team size | Warehouse fit | Main cost |
|---|---|---|---|---|
| Star schema | Mixed, open-ended BI; many facts sharing dimensions | Any | Any (columnar or row) | Query-time joins; more models to maintain |
| OBT (wide table) | A known set of hot dashboards at one grain | Small to mid | Columnar only (needs cheap wide scans) | Rigid grain; SCD/additivity footguns; rebuild cost |
| Data Vault core | Many volatile sources; audit and regulation dominate | Larger, dedicated | Any; shines where ingestion churns | Brutal to query — needs marts on top |
| Inmon / medallion core | Enterprise conformance must precede reporting | Larger | Any; native to lakehouses | Slow to first dashboard; 3NF isn’t query-facing |
| Activity schema | Behavioral, sequential, product-analytics questions | Small to mid | Columnar | Weak at classic aggregate reporting |
Read down that table and the pattern is hard to miss: the “core” rows and the “presentation” rows aren’t competitors — they stack. A serious warehouse is frequently a Data Vault or a medallion core feeding dimensional marts materialized as stars, OBTs, or both, with a semantic layer over the top. The choice is rarely star versus the field; it’s which shape at which layer, and the answer follows the workload and the team more than any doctrine.
And notice the one thing every row of that table needs to be filled in correctly. To build a good OBT you declare a grain and respect additivity. To hang a satellite you decide what a business key is. To conform a silver layer you make dimensions mean one thing. To derive a report from an activity stream you reconstruct measures and dimensions. Grain, conformance, additivity, and slowly changing history are the inputs to every column of the matrix — the physical shape changes; the questions you have to answer to get it right do not.
The verdict
So: with cheap storage, fast joins, and dbt, do you still need strict star schemas? Here’s the stance the whole series has been walking toward.
The physical rigidity of dimensional modeling has genuinely loosened, and pretending otherwise is nostalgia. Plenty of good teams now materialize their presentation layer as wide OBTs because their warehouse and their BI tool are happier that way, and they’re right to. Others ship a Data Vault core, or a medallion lakehouse, or an activity stream, and never draw a literal star at all. The strict, normalized-to-the-star, join-at-query-time physical model is no longer a mandate. If that’s what “Kimball” means to you, then yes — it has loosened its grip, and it should have.
But that was never the valuable part. The valuable part is the concepts, and they haven’t aged a day. Declaring the grain — what exactly one row means — before you write a line of SQL. Conformed dimensions, so “customer” means the same thing in every mart, every satellite, every layer of the medallion. Additivity, knowing which measures you can safely sum and which you can’t. Slowly changing dimensions, so history doesn’t quietly overwrite itself. These are not artifacts of 1996 hardware. They’re how you reason about analytical data at all, and every one of them survives being flattened into a wide table, decomposed into a Vault, or streamed as events — intact. When you build an OBT well, you’re making grain and additivity decisions the whole time; when you build one badly, it’s precisely because nobody declared the grain.
That’s the resolution. Star schema as a physical mandate has loosened. Star schema as a design discipline has not. You still model dimensionally even when you ship a wide table, because dimensional thinking is what tells you the table is correct. The flattening — or the Vault, or the medallion — is a materialization choice; the grain, the conformance, the additivity are the design, and the design is the durable part.
Which is the note to end the series on. The rest of this blog’s Data track is about the how — how to run dbt, what Snowflake does under your queries, how the modern data stack fits together. Powerful machinery, all of it. But machinery runs whatever you feed it, correct or not, and a fast pipeline that computes the wrong number is just a wrong number arriving sooner. Dimensional modeling is the what underneath all that how — the discipline that decides what one row means before the tools race off to build a million of them. Learn the tools; they’ll change. Learn to think in grain and conformed dimensions and additivity, and you’ll model well on whatever warehouse comes next — flattened, starred, vaulted, streamed, or something nobody’s named yet.
Comments