Skip to main content

Kimball vs One Big Table: The 10 GB Join-Side Heuristic

Level 7: Warehouses, Lakehouse & Dimensional Modelingmedium26 minOne Big Table patterndenormalization tradeoffsjoin-side sizingconformed dimensionsstorage cost arithmeticCASE classificationdecision frameworks

Decide star schema versus One Big Table per data mart using consumer needs and the ~10 GB join-side heuristic, then price what the denormalization actually costs.

The debate the canon skips

Everything you have rehearsed in this module is Kimball: a narrow fact surrounded by dimensions you join to. It is still the junior interview canon and it is still the right default. But it is not the only live answer in 2026, and the follow-up question is coming.

One Big Table (OBT) is the alternative: pre-join the dimensions into the fact once, at load time, and ship one very wide table. Every query becomes a single-table scan with no joins at all. Columnar engines make this viable in a way row stores never did, because a query that selects 4 columns out of 300 reads only those 4. BI teams love it: no join grammar to teach, no fan-out to explain, and dashboards get faster.

Adoption says it is a minority position and a real one. Practitioner surveys put mixed approaches at 36.8%, Kimball at 27.8%, ad-hoc at 17.4%, semantic models at 5.4%, One Big Table at 3.8%, and Data Vault at 3.3%. So Kimball is the default you should be able to build, and OBT is the tradeoff you should be able to argue.

Schema
dim_driver
  • driver_key
  • home_city
  • vehicle_class
fact_trips (star)
  • trip_id
  • driver_key
  • fare_usd
obt_trips (one big table)
  • trip_id
  • driver_key
  • home_city
  • vehicle_class
  • fare_usd
  • fact_trips (star)n-1dim_driverjoined at query time
Same data, two shapes. The star joins at read time and stores each driver attribute once. The OBT joins at load time and stores those attributes on every single trip row.

The heuristic that makes the answer sound experienced

The working rule from the modeling community: stay with Kimball while the join side is under roughly 10 GB. Under that, the engine can broadcast the dimension to every worker and the star join costs almost nothing. Past it, the dimension has to be shuffled across the network alongside the fact, and shuffle cost can run 10x to 100x the broadcast case. That is when pre-joining into an OBT starts earning its storage bill.

You have seen this physics before. In Module 7.1, DS_BCAST_INNER versus DS_DIST_BOTH in a query plan is the same fork, read off a plan after the fact. Here you are making the same call at design time, before anyone runs anything. Choosing a distribution key that co-locates the join is the third option in the same family.

When Kimball wins anyway, regardless of size

Three conditions override the size heuristic:

  1. Conformed dimensions. If dim_driver is joined by trips, payouts, and support tickets, an OBT means maintaining three copies of every driver attribute and hoping they agree. The whole point of a conformed dimension is that "home city" means one thing across the warehouse.
  2. History has to version. The SCD2 machinery from the last lesson has nowhere to live in an OBT. There is no dimension row to close and reopen, so a driver's move either rewrites history everywhere or is lost.
  3. Dimension attributes change often. In an OBT, changing one attribute on one driver rewrites every fact row that driver ever appeared in. On a billion-row table that is not an update, it is a rebuild.

Common mistake: treating the storage growth as the whole cost and stopping there. Storage is the cheap part. The expensive part is that an OBT converts a small dimension update into a large fact rewrite, and it converts one shared definition into several private copies that drift.

Interview nuance: "would you just make one big table?" is a real follow-up to any star-schema answer, and it is a judgment probe, not a knowledge probe. Answering "no, Kimball is correct" reads as dogma. Answering "OBT, it is faster" reads as inexperience. The strong answer names the number and one cost of each path: "I would keep the star while the dimension side stays under about 10 GB, since the engine broadcasts it and the join is nearly free. Past that I would pre-join into a wide table, and I would accept that a dimension change now rewrites the fact and that these attributes stop being conformed."

On a real platform this differs. Real OBTs usually are not flat. Snowflake, BigQuery, and Databricks all support nested ARRAY<STRUCT> columns, so a trips table can carry the driver's attributes as one nested struct and the trip's waypoints as an array, keeping the single-table scan without exploding the row count. And the decision is rarely permanent: teams often keep the Kimball star as the modeled layer and materialize an OBT on top of it for BI, which buys the fast dashboards without giving up the conformed dimensions or the version history.

Sample data for this example
CREATE TABLE mart_profiles (
  mart                  TEXT,
  fact_rows             INTEGER,
  fact_row_bytes        INTEGER,  -- bytes per fact row in the star (narrow: measures + keys)
  denorm_extra_bytes    INTEGER,  -- bytes per row ADDED by pre-joining the dimensions in
  fact_gb               REAL,     -- fact_rows * fact_row_bytes, in GB (1 GB = 1e9 bytes)
  total_dim_gb          REAL,     -- the join side: every dimension this fact joins to
  dims_reused_elsewhere INTEGER,  -- 1 when other facts conform to these same dimensions
  dim_change_rate_pct   REAL,     -- percent of dimension attributes that change per cycle
  bi_only               INTEGER   -- 1 when a BI tool is the only consumer of this mart
);
INSERT INTO mart_profiles (mart, fact_rows, fact_row_bytes, denorm_extra_bytes, fact_gb, total_dim_gb, dims_reused_elsewhere, dim_change_rate_pct, bi_only) VALUES
  ('ride_share_trips',      2400000000,  96, 140, 230.4,  3.2, 1, 6.0, 0),
  ('marketing_attribution',  180000000, 120, 210,  21.6, 14.5, 0, 1.5, 1),
  ('finance_gl',              90000000, 140, 160,  12.6,  6.4, 1, 2.0, 0),
  ('support_tickets',         40000000, 110, 180,   4.4,  7.3, 0, 1.0, 1),
  ('driver_payouts',         300000000,  90, 130,  27.0, 11.8, 0, 9.0, 0),
  ('app_events',            5000000000,  72, 190, 360.0, 18.7, 0, 0.5, 1),
  ('fleet_maintenance',       12000000, 125, 160,   1.5,  6.1, 0, 3.0, 1),
  ('rider_ltv',              260000000, 100, 240,  26.0, 12.4, 1, 4.0, 1);
Worked example (SQL)
-- The join side is the number the heuristic is about. Which marts are over the line?
SELECT mart, fact_gb, total_dim_gb,
       CASE WHEN total_dim_gb > 10 THEN 'over 10 GB: shuffle territory'
            ELSE 'under 10 GB: broadcast territory' END AS join_side
FROM mart_profiles
ORDER BY total_dim_gb DESC;

Apply

Your turn

The task this lesson builds to.

Write a query that classifies each mart as (mart, recommendation) over mart_profiles(mart, fact_rows, fact_row_bytes, denorm_extra_bytes, fact_gb, total_dim_gb, dims_reused_elsewhere, dim_change_rate_pct, bi_only), ordered by mart.

Apply these rules in this order:

  1. 'obt' when total_dim_gb > 10 and bi_only = 1 and dims_reused_elsewhere = 0.
  2. 'star' when dims_reused_elsewhere = 1 or dim_change_rate_pct > 5.
  3. 'either' otherwise.

Alias the classification exactly recommendation.

4 hints and 1 automated check are waiting in the workspace.

Practice

Make it stick

A second problem on the same idea, plus 2 bonus drills.

Write a query that prices the denormalization for every mart as (mart, star_gb, obt_gb, growth_factor) over the same mart_profiles table.

star_gb is the stored fact_gb. obt_gb is fact_rows times (fact_row_bytes + denorm_extra_bytes), converted to GB at 1 GB = 1,000,000,000 bytes. growth_factor is (fact_row_bytes + denorm_extra_bytes) divided by fact_row_bytes. Round all three to 1 decimal, and order by growth_factor descending then mart.

4 hints and 1 automated check are waiting in the workspace.