A snowflake schema is a star schema where the dimension tables are themselves normalized — usually to 3NF. Instead of one wide dim_product table, you'd have dim_product joining to dim_category joining to dim_department.
fct_orders
│
▼
dim_product
│
▼
dim_category
│
▼
dim_department
Visually: the central fact, with dimensions that themselves branch into sub-tables — like a snowflake.
Star schema reminder
In a pure star, dim_product has its category and department right on it:
dim_product(product_key, product_name, category_name, department_name, ...)
One join from fact to product → all product attributes available.
Snowflake's promise
- Less storage —
department_namelives in one row per department, not duplicated across every product. - Cleaner updates — change a department name once instead of in 10,000 product rows.
- Conformed sub-dimensions —
dim_categoryis reusable across other dimensions (if multiple dimensions reference categories).
Star's promise
- Fewer joins — one fact-to-dimension join instead of three. Important for engines that don't optimize multi-hop joins.
- Simpler model — easier for analysts to learn. No "wait, which table has department_name again?"
- Better star-join optimization — warehouse engines optimize the star pattern explicitly.
Why star usually wins in modern warehouses
- Storage is cheap — columnar compression makes the duplication-cost of denormalization negligible.
- Joins are cheap-ish — but adding extra joins still slows queries.
- Analysts prefer flat tables — pivoting through three joins to get department is friction.
- Update cost is rare — warehouses bulk-load; the "change department name in 10,000 rows" is a once-a-year non-issue, easy to script.
The exceptions where snowflake makes sense:
- Genuinely shared sub-dimensions — if
dim_categoryis referenced bydim_product,dim_supplier, anddim_employee, normalizing into a shared category dim avoids triplicate definitions. - Very high-cardinality dimensions — a
dim_productwith 1M rows where most rows share a small category set can save real space. - Rarely-queried attributes — putting them in a sub-dimension keeps the main dim narrow.
The pragmatic answer
In practice, most modern warehouses use a slightly-snowflaked star: the main dimension is denormalized, but very small sub-dimensions (like calendar metadata, currency reference) live in their own tables.
fct_orders → dim_customer (flat: country, segment, signup_date, ...)
fct_orders → dim_product (flat: category_name, department_name, brand_name, ...)
fct_orders → dim_date (the calendar — flat)
But:
dim_product → dim_brand (only normalized if brand is shared across products + suppliers + customers)
Almost everything is star; the few snowflaked dimensions are deliberate exceptions for shared / large dimensions.
When you inherit a snowflake
If you join an existing warehouse and find heavily snowflaked dimensions, the question is: is it deliberate (good reason) or accidental (someone modelled OLTP-style)?
- Deliberate: documented sharing across multiple parent dimensions, consistent application.
- Accidental: every dimension is over-normalized, queries have 12+ joins, analysts complain.
Accidental snowflakes are the most common warehouse smell to inherit. The fix is incremental denormalization of the most-queried dimensions into flat tables.
Common mistakes
- Snowflaking by reflex — "normalization is good" applied to OLAP where it usually isn't.
- Denormalizing too aggressively — sometimes a 50-column flat dim is harder to maintain than 3 normalized tables. Use judgment.
- Mixing styles within one model — half the dimensions snowflaked, half flat. Inconsistent, hard to learn.
Takeaway
Default to star. Snowflake only sub-dimensions that are genuinely shared, very large, or rarely queried. The modern warehouse engine's design assumes star — fight it at your peril.