Star schema vs snowflake schema
One fact table in the middle. Dimensions flat or normalised.
A star schema puts one fact table in the middle with denormalised dimension tables hanging off it. A snowflake schema does the same but normalises the dimensions further into smaller tables. Star reads faster. Snowflake stores tighter. On a modern columnar warehouse, star wins almost every time.
The two shapes
flowchart LR
subgraph S["Star"]
F1[("fact_sales")]:::b
D1[("dim_product<br/>(flat)")]:::g
D2[("dim_customer<br/>(flat)")]:::g
D3[("dim_date<br/>(flat)")]:::g
F1 --- D1
F1 --- D2
F1 --- D3
end
subgraph SN["Snowflake"]
F2[("fact_sales")]:::b
DP[("dim_product")]:::g
DC[("dim_category")]:::g
DB[("dim_brand")]:::g
DCG[("dim_category_group")]:::g
F2 --- DP
DP --- DC
DP --- DB
DC --- DCG
end
classDef b fill:#fed7aa,stroke:#c2410c,color:#7c2d12
classDef g fill:#dcfce7,stroke:#15803d,color:#14532d
In a star, dim_product has every product attribute on it: product_name, category_name, category_group, brand_name. In a snowflake, those higher-level attributes get pushed out to dim_category, dim_brand, dim_category_group. The fact joins through the product dim, which joins through the category dim, and so on.
Worked example
A star dim_product:
1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE dim_product (
product_sk bigint primary key,
product_id text,
product_name text,
category_id text,
category_name text,
category_group text,
brand_id text,
brand_name text,
list_price numeric
);
The snowflake version splits it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE dim_product (
product_sk bigint primary key,
product_id text,
product_name text,
category_sk bigint references dim_category,
brand_sk bigint references dim_brand,
list_price numeric
);
CREATE TABLE dim_category (
category_sk bigint primary key,
category_id text,
category_name text,
category_group_sk bigint references dim_category_group
);
A “sales by category” query on the star is one join. On the snowflake it is three.
Why star usually wins
Two reasons, both about modern warehouses.
First, columnar storage. Repeating category_name = 'Shoes' on a million product rows costs almost nothing. Run-length encoding crushes the column down to a handful of bytes. The “wasted space” argument for snowflake assumed row storage from 1995.
Second, joins are not free. Snowflake schemas force extra joins on every query. On a 10 TB fact table, one extra hash join is real work. Three extra joins is a noticeable bill.
flowchart LR
Q["Sales by category"]:::a
Q --> ST["Star:<br/>fact JOIN dim_product<br/>(1 join)"]:::g
Q --> SN["Snowflake:<br/>fact JOIN dim_product<br/>JOIN dim_category<br/>JOIN dim_category_group<br/>(3 joins)"]:::y
classDef a fill:#dbeafe,stroke:#1e40af,color:#1e3a8a
classDef g fill:#dcfce7,stroke:#15803d,color:#14532d
classDef y fill:#fef3c7,stroke:#a16207,color:#713f12
When snowflake actually makes sense
A short list, because it is genuinely short.
- Regulated industries where the category hierarchy is its own source of truth, owned by a different team, and must be modelled separately for audit reasons.
- Very large dimensions (hundreds of millions of rows) where the saved storage on a column with low cardinality is meaningful.
- Dimension reuse across many marts where the same brand or category lookup is referenced from a dozen places and the team wants exactly one place to update it.
- MDM-backed reference data where the hierarchy lives in a master data system and the warehouse is mirroring it.
For most teams none of these apply. Flatten the dim, ship the star, move on.
The galaxy / fact constellation
Real warehouses have more than one fact table. Orders, payments, shipments, support tickets. They share dimensions (customer, product, date). That shape is sometimes called a galaxy or a fact constellation.
flowchart LR
DC[("dim_customer")]:::g
DP[("dim_product")]:::g
DD[("dim_date")]:::g
F1[("fact_orders")]:::b
F2[("fact_payments")]:::b
F3[("fact_shipments")]:::b
DC --- F1
DC --- F2
DC --- F3
DP --- F1
DP --- F3
DD --- F1
DD --- F2
DD --- F3
classDef g fill:#dcfce7,stroke:#15803d,color:#14532d
classDef b fill:#fed7aa,stroke:#c2410c,color:#7c2d12
The shared dims here are conformed dimensions (see Conformed dimensions). Getting them right is what makes the galaxy work; getting them wrong is what makes “customer count” disagree across reports.
Cost of denormalisation
The thing that genuinely costs money on a star is rebuilding the flat dim when an attribute changes. If you rename a category, every dim_product row for that category has to update. On a snowflake you update one row in dim_category.
In practice this matters less than it sounds. Dimensions are small. A dim_product with a million rows rebuilds in seconds on any decent warehouse. The fact rebuild is the expensive part, and that does not change between star and snowflake.
Common mistakes
- Reaching for snowflake “because it’s more normalised.” Normalisation is a constraint for transactional systems. The warehouse has different goals: fast scans, simple queries, easy BI. Star fits those goals better.
- Building a snowflake to save storage on columnar. Run-length encoding already saves that storage. You are paying with extra joins for nothing.
- Flattening a dim that has a genuine many-to-many. A product belongs to many categories? Do not flatten; use a bridge table or accept one row per (product, category).
- Forgetting that snowflake makes BI tools harder. Looker, Tableau, Power BI all assume star-shaped joins. A snowflake forces extra join steps in every explore.
- Treating the choice as global. Most marts are clean stars. One regulated mart can still be a snowflake. Pick per mart, not per warehouse.
- Modelling “in case we need it later.” Build the star you need now. Snowflake-ing later is easy; un-snowflaking is the hard direction.
Quick recap
- Star: fact in the middle, flat denormalised dims. The default on columnar warehouses.
- Snowflake: dims normalised into their own dim tables. More joins, less duplication.
- Modern columnar storage makes the snowflake storage argument almost irrelevant.
- Galaxy / fact constellation: multiple facts sharing conformed dims. The shape of a real warehouse.
- Snowflake only when a dim is regulated, huge, or shared across many marts that all need the same updates.
This concept sits in Stage 2 (Data modeling and warehousing) of the Data Engineering Roadmap.
Last updated