Chapter 08

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.

Reading: ~45 min Interactive: 2 widgets Source: Polimi TIS 2025/26 — DW Logical Design (deck 10)

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.

Q

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 did_i 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 (d1,d2,,dn)(d_1, d_2, …, d_n) — 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 → category is 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, Type

The 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 — DIMENSIONS DENORMALIZED SNOWFLAKE — DIMENSIONS NORMALIZED PRODUCT ID_Product Product Type Category SALE ID_Product ID_Shop ID_Week Quantity SHOP ID_Shop Shop City State WEEK ID_Week Week Month Year Category is 1 join away Type repeats once per product, State once per shop — redundancy, bought on purpose. TYPE ID_Type Type ID_Category CATEGORY ID_Category Category PRODUCT ID_Product Product ID_Type SALE ID_Product ID_Shop ID_Week Quantity SHOP ID_Shop Shop ID_City CITY ID_City City State WEEK ID_Week Week Month Year Category is 3 joins away Each Type and State stored once — less space, more joins to reach outer levels. a warehouse is read-only, so the star's redundancy costs nothing it would otherwise cost — that is why ROLAP defaults to the star
×

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.

2016-02-23-q12016Q01Data marts & DW design methodologymedium7 pts
Define what a Data Mart is in a Data Warehouse and clearly summarize the methodological steps that lead from a collection of datasets to the specification of the logical schemas of one or more Data Marts.

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.

Part II query toolkit The four exam SQL patterns

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:

Hands-on

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
Grouping sets
8
Result rows
48
Plain GROUP BY
18

WITH CUBEemits 8 grouping sets over 3 attributes 2^3, turning 18 plain rows into 48.

MonthZoneProductSUM(quantity)
Februarynorthpasta15,000
Februarynorthrice9,000
Februaryeastpasta17,000
Februaryeastrice8,000
Februarycenterpasta13,000
Februarycenterrice7,000
Marchnorthpasta18,000
Marchnorthrice9,500
Marcheastpasta17,000
Marcheastrice8,500
Marchcenterpasta15,000
Marchcenterrice7,000
Aprilnorthpasta18,000
Aprilnorthrice10,000
Aprileastpasta18,000
Aprileastrice9,000
Aprilcenterpasta15,000
Aprilcenterrice8,000
FebruarynorthALL24,000
FebruaryeastALL25,000
FebruarycenterALL20,000
MarchnorthALL27,500
MarcheastALL25,500
MarchcenterALL22,000
AprilnorthALL28,000
AprileastALL27,000
AprilcenterALL23,000
FebruaryALLpasta45,000
FebruaryALLrice24,000
MarchALLpasta50,000
MarchALLrice25,000
AprilALLpasta51,000
AprilALLrice27,000
ALLnorthpasta51,000
ALLnorthrice28,500
ALLeastpasta52,000
ALLeastrice25,500
ALLcenterpasta43,000
ALLcenterrice22,000
FebruaryALLALL69,000
MarchALLALL75,000
AprilALLALL78,000
ALLnorthALL79,500
ALLeastALL77,500
ALLcenterALL65,000
ALLALLpasta146,000
ALLALLrice76,000
ALLALLALL222,000
Try thisWith all three attributes selected, flip between WITH CUBE (8 sets, 48 rows) and WITH ROLLUP (4 sets, 31 rows). Then, still on ROLLUP, remove Month and re-add it — the hierarchy it walks changes with the argument order, and so does every row.
TakeawayRead the exam text before choosing: "including all sub-aggregations" means every combination — CUBE. "By date, by month, by year" walks one hierarchy — ROLLUP, and the argument order is that hierarchy. A named handful of levels and nothing else — GROUPING SETS.
×

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:

2017-07-03-q32017Q03DW design (full pipeline)hard22 pts
PoliMultiplex is an Italian multiplex chain; every ticket is tied to the fidelity-card customer who bought it. The operational DB stores theaters, showing rooms, showings (each with its own price), movies and their main actors. Design a data warehouse to analyze the issued tickets. Operational database: - CITY(CityName, Region) - CUSTOMER(CustomerId, Name, HomeCityName, BirthYear) - THEATER(TheaterId, TheaterName, CityName) - SHOWINGROOM(TheaterId, RoomNr, NrSeats) - MOVIE(MovieId, Title, Genre, DurationInMinutes, ProductionYear) - SHOWING(ShowingId, Date, Time, TheaterId, RoomNr, MovieId, Price) - ACTOR(ActorId, ActorName, Gender, BirthYear, HomeCountry) - STARRING(MovieId, ActorId), TICKET(CustomerId, ShowingId) 1. (3 pts) Reverse-engineer the logical schema into an ER conceptual schema. 2. For the useful fact(s): a. (3 pts) attribute tree with pruning/grafting; b. (3 pts) fact schema; c. (2 pts) glossary. 3. (3 pts) Logical schema. 4. SQL: a. (2 pts) for customers from Rome — total income by date, month and year, including the aggregations by each attribute alone; b. (2 pts) total tickets per actor (id and name), customer home region, customer birth year and theater region; c. (2 pts) total income per movie (id and title), only for movies starring at least one actress; d. (2 pts) for Milan theaters — per day of week and theater (id and name), the genre(s) with the greatest number of tickets.

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 — vivjv_i \le v_j iff viv_i is less aggregated, i.e. vjv_j‘s data can be computed from viv_i‘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:

Hands-on

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.

Rows scanned
144
vs base only
144
Rows stored
0

Nothing materialized: every query scans the 18-row base. Correct, and as slow as it gets.

Query grouped byanswered fromrows readbenefit if added
month,zone,productmonth,zone,product18
month,zonemonth,zone,product1836
month,productmonth,zone,product1848
zone,productmonth,zone,product1848
monthmonth,zone,product1830
zonemonth,zone,product1830
productmonth,zone,product1832
()month,zone,product1817
Try thisMaterialize () — 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.
TakeawayA view can answer any query at least as coarse as itself, which is why the lattice is a containment order and not a list. Selection is a genuine trade-off — more storage and slower refresh against faster queries — and the greedy rule is to buy the view with the highest benefit, not the smallest size. This only works for distributive operators like SUM; a holistic one such as MEDIAN cannot be derived from a coarser view at all.
tip

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).

key

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) vs WITH ROLLUP (one hierarchy); greatest-per-group via a correlated MAX subquery; weighted re-aggregation of an average (SUM(cnt·avg)/SUM(cnt)); bridge-table weighted vs impact queries.
  • Materialized views on the MD lattice (vivjv_i \le v_j); 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.