Logical DW Design: Star, Snowflake & OLAP SQL
The logical and SQL half of every Part II warehouse exercise. Turning a fact schema into a ROLAP star (or snowflake) schema with surrogate keys and bridge tables, the four SQL patterns graded in every exercise (CUBE/ROLLUP, greatest-per-group, weighted re-aggregation of averages, bridge-table queries), and choosing which materialized views to precompute on the multidimensional lattice.
01 · Model
Star & snowflake schemas
A warehouse is normally ROLAP — the multidimensional cube stored in relational tables (as opposed to MOLAP’s native, proprietary, sparse physical cube, or HOLAP’s hybrid). The de-facto ROLAP logical model is the star schema.
Logical design — asked as theory, and the second half of every DW exercise
“Data marts and the DW design methodology” is a Part I question (2016-02-23), and the star/snowflake translation + SQL is the logical half of the DW-design Part II in 9 of 16 sessions. Everything here is directly graded.
A star schema is:
Dimension tables DT₁…DTₙ
One per dimension, each with a surrogate primary key and the attributes describing that dimension at all its aggregation levels.
One fact table FT
Imports the dimension keys — its primary key is the combination — and holds one attribute per measure.
Two design choices define the star:
- Surrogate keys. Dimension keys are generated ids, not the operational keys — smaller, so joins and the fact table are cheaper.
- Denormalization. Dimension tables are denormalized:
product → type → categoryis a transitive dependency that a star keeps in one table. That is redundancy — but it means fewer joins, and since a warehouse has no updates, the usual anomaly risk does not bite.
A typical OLAP query joins the fact to its dimensions, filters, and groups:
SELECT City, Week, Type, SUM(Quantity)
FROM Week, Shop, Product, Sale
WHERE Week.ID_Week = Sale.ID_Week AND Shop.ID_Shop = Sale.ID_Shop
AND Product.ID_Product = Sale.ID_Product AND Product.Category = 'FoodStuff'
GROUP BY City, Week, TypeThe snowflake schema normalizes the dimension tables, removing some transitive
dependencies into secondary tables: a SHOP table keeps only ID_City as an external key, and
CITY(ID_City, City, State) is separate — so a shop’s state is stored once per city, not once per
shop. It saves space and helps queries on the fact plus primary dimension attributes, at the cost of
extra joins to reach the outer levels.
Both schemas, drawn from the same SALE fact, so the only difference visible is the one that matters
— where the hierarchy levels live, and what that costs to query:
Star vs snowflake — the trade-off graders want named
Star = fully denormalized dimensions → redundancy, but the fewest joins (the default for OLAP). Snowflake = (partially) normalized dimensions → less redundancy/space, but more joins to reach outer hierarchy levels. Saying “snowflake is better because it’s normalized” misses the point: in a read-only warehouse, the star’s redundancy is a feature, bought for speed.
02 · Exam-hot
OLAP SQL: the four Part II patterns
Every DW-design Part II ends with a set of SQL queries (usually four), and they draw from the same small toolkit. Master these four patterns and the query section writes itself; miss one and you drop the marks it carries.
1 · All-combination aggregation — WITH CUBE / ROLLUP / GROUPING SETS
“Total X by A, B, C including all sub-aggregations” → GROUP BY A, B, C WITH CUBE (every one-
and two-attribute combination). If instead the request follows one hierarchy (“by date, by
month, by year”), use WITH ROLLUP (hierarchical prefixes only). For a specific subset, use
GROUPING SETS.
2 · Greatest-per-group — a correlated subquery
“For each series, the conference with the greatest number of registrants” → build a view of the
per-group aggregate, then keep the rows equal to the MAX within their group:
3 · Weighted re-aggregation of an average
An AVG measure stored in the fact cannot be plainly re-averaged at a coarser level — weight it
by its count: SUM(cnt · avg) / SUM(cnt).
4 · Bridge-table (many-to-many) queries
A multiple edge becomes a bridge table with a WEIGHT. A weighted query multiplies by the
weight; an impact query ignores it.
-- Pattern 2: greatest-per-group (per series, the conference(s) with the most registrants)
CREATE VIEW ConfNum(ConferenceKey, Series, Year, Num) AS (
SELECT C.ConferenceKey, C.Series, C.Year, SUM(R.NumOfRegistrants)
FROM Registrations R, Conference C
WHERE R.ConferenceKey = C.ConferenceKey
GROUP BY C.ConferenceKey, C.Series, C.Year
);
SELECT C.Series, C.Year
FROM ConfNum C
WHERE C.Num = (SELECT MAX(C2.Num) FROM ConfNum C2 WHERE C2.Series = C.Series);
-- Pattern 3: weighted re-aggregation of an AVG measure (avg discount by continent & day)
SELECT U.Continent, D.DayOfWeek,
SUM(R.NumOfRegistrants * R.AvgDiscount) / SUM(R.NumOfRegistrants)
FROM Registrations R, Country U, Date D
WHERE R.UserCountry = U.CountryName AND R.Date = D.Date
GROUP BY U.Continent, D.DayOfWeek;
-- Pattern 4: bridge table — weighted vs impact
SELECT A.Author, SUM(S.Profit * B.Weight) -- weighted: profit apportioned by weight
FROM Authors A, Bridge B, Books Bk, Sales S
WHERE A.Author_id = B.Author_id AND B.Book_id = Bk.Book_id AND Bk.Book_id = S.Book_id
GROUP BY A.Author;Pattern 1 is the one that costs marks most often, so run the three operators against a real fact table and count what each emits — the distinction is not in their definitions, it is in their output:
CUBE, ROLLUP & GROUPING SETS — what each one emits
Same fact table, same measure, three operators. Pick the grouping attributes in the order you want them, then switch operators and watch the grouping sets — and the result rows — change. The row count is the whole distinction.
Use it when the request says "including all sub-aggregations" — every combination of the attributes.
SELECT month, zone, product, SUM(quantity) FROM Sales GROUP BY month, zone, product WITH CUBE
WITH CUBEemits 8 grouping sets over 3 attributes — 2^3, turning 18 plain rows into 48.
| Month | Zone | Product | SUM(quantity) |
|---|---|---|---|
| February | north | pasta | 15,000 |
| February | north | rice | 9,000 |
| February | east | pasta | 17,000 |
| February | east | rice | 8,000 |
| February | center | pasta | 13,000 |
| February | center | rice | 7,000 |
| March | north | pasta | 18,000 |
| March | north | rice | 9,500 |
| March | east | pasta | 17,000 |
| March | east | rice | 8,500 |
| March | center | pasta | 15,000 |
| March | center | rice | 7,000 |
| April | north | pasta | 18,000 |
| April | north | rice | 10,000 |
| April | east | pasta | 18,000 |
| April | east | rice | 9,000 |
| April | center | pasta | 15,000 |
| April | center | rice | 8,000 |
| February | north | ALL | 24,000 |
| February | east | ALL | 25,000 |
| February | center | ALL | 20,000 |
| March | north | ALL | 27,500 |
| March | east | ALL | 25,500 |
| March | center | ALL | 22,000 |
| April | north | ALL | 28,000 |
| April | east | ALL | 27,000 |
| April | center | ALL | 23,000 |
| February | ALL | pasta | 45,000 |
| February | ALL | rice | 24,000 |
| March | ALL | pasta | 50,000 |
| March | ALL | rice | 25,000 |
| April | ALL | pasta | 51,000 |
| April | ALL | rice | 27,000 |
| ALL | north | pasta | 51,000 |
| ALL | north | rice | 28,500 |
| ALL | east | pasta | 52,000 |
| ALL | east | rice | 25,500 |
| ALL | center | pasta | 43,000 |
| ALL | center | rice | 22,000 |
| February | ALL | ALL | 69,000 |
| March | ALL | ALL | 75,000 |
| April | ALL | ALL | 78,000 |
| ALL | north | ALL | 79,500 |
| ALL | east | ALL | 77,500 |
| ALL | center | ALL | 65,000 |
| ALL | ALL | pasta | 146,000 |
| ALL | ALL | rice | 76,000 |
| ALL | ALL | ALL | 222,000 |
The three ways students lose SQL marks
(1) CUBE where ROLLUP is meant (or vice-versa) — read whether the request is all combinations or
one hierarchy. (2) Plain AVG of an average measure at a coarser level — it must be
count-weighted, or recomputed from the underlying flows. (3) Forgetting the bridge weight on a
weighted query (or applying it on an impact query like “sold copies per author”). All three are
flagged in the worked solutions’ misconceptions.
Here is a full DW-design exercise whose four SQL queries exercise CUBE/GROUPING-SETS, a bridge (actor) axis, an EXISTS filter, and a greatest-per-group — the whole toolkit at once:
03 · Optimization
Materialized views & the MD lattice
Aggregation is expensive, so a warehouse precomputes it: a materialized view is a fact table holding more-aggregated data. Views form a multidimensional lattice ordered by aggregation — iff is less aggregated, i.e. ‘s data can be computed from ‘s. Whether you can roll a view up cheaply depends on the measure’s operator:
Distributive
Aggregate from partial aggregates directly — SUM, MAX, MIN. The friendly case.
Algebraic
Needs extra support measures — AVG = SUM / COUNT, so you must carry both. (This is why
averages need weighted re-aggregation.)
Holistic
Cannot be computed from partial aggregates at all — MEDIAN. Must go back to the primary data.
Given a distributive measure, which views are worth precomputing is a real trade-off with a counter-intuitive answer — and the smallest view is almost always the worst buy:
The MD lattice — which views to materialize
Every node is a possible pre-computed aggregation. A query can be answered from any view at least as fine as itself — never from a coarser one. The base (top) is always there, so materializing nothing is correct and merely slow.
Nothing materialized: every query scans the 18-row base. Correct, and as slow as it gets.
| Query grouped by | answered from | rows read | benefit if added |
|---|---|---|---|
| month,zone,product | month,zone,product | 18 | — |
| month,zone | month,zone,product | 18 | 36 |
| month,product | month,zone,product | 18 | 48 |
| zone,product | month,zone,product | 18 | 48 |
| month | month,zone,product | 18 | 30 |
| zone | month,zone,product | 18 | 30 |
| product | month,zone,product | 18 | 32 |
| () | month,zone,product | 18 | 17 |
() — the smallest node in the lattice, one row. It buys almost nothing, because it answers only itself. Now clear and materialize month,zone instead: bigger, but it serves itself, month, zone and (). Storage is not the criterion — descendants served per row stored is.SUM; a holistic one such as MEDIAN cannot be derived from a coarser view at all.Derived measures don't re-aggregate like measures
Profit = Quantity × Price computed per product does not re-aggregate by summing the per-product
profits blindly — the correct value comes from aggregating the primary data. This is the same trap as
the AVG measure: derived and algebraic measures need the underlying values, not the pre-aggregated ones.
The logical-design workflow is four steps — choose the schema (star/snowflake) → translate the conceptual schema → choose materialized views → optimize — driven by the workload (dynamic, extemporaneous OLAP queries) and the data volume (distinct values × attribute size × number of events). Translating the fact schema follows fixed guidelines: descriptive attributes go in the dimension table of the attribute they hang off (or in the fact if they hang off the fact); optional attributes get nulls/ad-hoc values; cross-dimensional attributes get a new table keyed by the dimensional attributes they combine; shared hierarchies / convergence must not duplicate the dimension table (use roles); and multiple edges become a bridge table (or a new dimension, if you keep a star).
Which views to materialize
View selection balances contrasting costs: workload cost and view-maintenance cost against disk space, update time, and the users’ max answer time and freshness needs. A view is worth materializing when it directly solves a frequent query or reduces the cost of several; it is not worth it when its aggregation pattern duplicates an existing view or does not lower cost. On the lattice you pick among exact views (solve one query), less-refined views (solve several), and candidate views — trading query-execution cost against disk space against view-computation time. Storing aggregate data can also use a unique fact table (nulls for finer levels) or a constellation schema (separate fact tables per pattern, sharing dimensions).
Load-bearing ideas
- ROLAP star schema: denormalized dimension tables (surrogate keys) around one fact table (PK = the dimension keys, one column per measure); redundancy bought for fewer joins. Snowflake normalizes the dimensions — less space, more joins.
- The four Part II SQL patterns:
WITH CUBE(all combinations) vsWITH ROLLUP(one hierarchy); greatest-per-group via a correlatedMAXsubquery; weighted re-aggregation of an average (SUM(cnt·avg)/SUM(cnt)); bridge-table weighted vs impact queries. - Materialized views on the MD lattice (); distributive (SUM/MAX/MIN) roll up freely, algebraic (AVG) need support measures, holistic (MEDIAN) cannot — the reason averages and derived measures must re-aggregate from primary data.
- Translation guidelines: cross-dimensional → new table; shared hierarchy → roles (no duplication); multiple edge → bridge table (or extra dimension). Aggregate storage: unique fact table (nulls) or constellation.
- Exam radar: (1) star vs snowflake with the join/space trade-off; (2) the four SQL patterns — especially CUBE-vs-ROLLUP and the weighted average — which together carry the Part II query marks.