π― Objective: a warehouse is Subject-Oriented β one star schema focuses on one subject only; a second fact table is created for exactly two reasons β a different subject or a different granularity.
π¦ Core Components:different subject β Book Sales vs Review β the applicable dimension sets differ | different granularity β Car Service β Service grain vs Part grain.
β‘ Key Constraint: the dimensions identify the subject, not the unit of measure β kilometres, litres and dollars measured against the same three dimensions are ONE fact with three measures, never three facts.
π How It Works
1. When a second fact is justified
Four warehouse features β Integrated Β· Subject-Oriented Β· Time-Variant Β· Non-Volatile; the second one is what forces the rule.
Two causes only β (i) different subject, (ii) different granularity; a measure being in different units is not a cause.
Dimensions are shared, not duplicated β both facts hang off the same DIM tables wherever they apply; only the set of applicable dimensions differs.
Where the granularity cause comes from(W8) β materialising a dimension hierarchy as one fact per level is the systematic source of granularity multi-facts; two hierarchies give βiββiβ facts and the whole thing is a Fact Constellation β Fact Constellation, Dimension Hierarchies.
2. The subject test β the measure Γ dimension applicability grid (Book Sales)
Method β cross every candidate fact measure with every candidate dimension and ask whether the pairing answers a sensible business question; a measure that fails a dimension cannot sit in that dimensionβs fact.
Where Num_of_Reviews fails β Store (a star rating belongs to the book, not the shop that sold it) and Time (the recorded date is the purchase date, not the review date β read off the E/R diagram, not assumed).
The split β BookSalesFACT(StoreIDβ,CategoryIDβ,TimeIDβ,StarIDββ,Num_of_Books,Total_Sales) and ReviewFACT(CategoryIDβ,StarIDββ,Num_of_Reviews), sharing CategoryDIM and StarRatingDIM.
One rating per book β a book receives many reviews, so StarID in the sales fact is the bookβs rounded average rating banded into the star dimension, not an individual review.
Lab 7 evidence β source tables live in the dtaniar account (BOOK5, REVIEW5, SALES5, SALESDETAILS5, β¦); ISBN 0316465186 is the book with no review, and it is the row that proves the outer join and the nvl(β¦, 0) both fired.
3. Different granularity β Car Service
Star-1, Service grain β CarServiceFACT1β(TimeIDβ,BrandNameβ,ServiceNoβ,MechanicIDββ,Total_Service_Cost,Number_of_Services); PartDIM reaches it only through a ServiceBridge.
Star-2, Part grain β CarServiceFACT2β(TimeIDβ,BrandNameβ,PartNoβ,MechanicIDββ,Number_of_Services) β finer detail, the bridge disappears, PartNo enters the key.
PartDIM must be a determinant dimension β one part serves many services, so a query that omits PartNo double-counts Number_of_Services; the star draws it dashed.
Total_Service_Cost cannot follow β cost is recorded per service, and the E/R diagram gives no per-part cost, so the finer fact carries the count measure only.
βοΈ Core Implementation
πΉ Book Sales β the four dimensions
copy Β· project-and-derive Β· hand-build
create table CategoryDim as select * from Category;create table StoreDim as select * from Store;create table TimeDim asselect distinct to_char(SalesDate, 'YYYYMM') as TimeID, to_char(SalesDate, 'MM') as Month, to_char(SalesDate, 'YYYY') as Yearfrom Sales;create table StarRatingDim (StarID number(1), StarDescription varchar2(15));insert into StarRatingDim values (0, 'Unknown'); -- 1 Poor / 2 Not Good / 3 Averageinsert into StarRatingDim values (5, 'Excellent'); -- 4 Good
π‘ Common Mistake:Deriving StarRatingDim from Review β it is hand-built precisely so that books with no review still have a member (0=Unknown) to point at β Building Dimension Tables.
πΉ The small fact is the easy one
ReviewFACT β a plain count over the join
create table ReviewFact asselect B.CategoryID, R.Stars as StarID, count(*) as Num_of_Reviewsfrom Book B, Review Rwhere B.ISBN = R.ISBNgroup by B.CategoryID, R.Stars;
πΉ The large fact needs one star rating per book first
create table TempBookWithStar as -- keep books that have NO reviewselect B.ISBN, B.CategoryID, nvl(R.Stars, 0) as Starsfrom Book B, Review Rwhere B.ISBN = R.ISBN(+); -- ANSI: Book B left outer join Review R on ...create table TempBookWithAvgStar as -- collapse many reviews to ONE ratingselect ISBN, CategoryID, round(avg(Stars)) as Avg_Starsfrom TempBookWithStargroup by ISBN, CategoryID;create table BookSalesFact as -- now the book joins the sales lines onceselect T.CategoryID, to_char(S.SalesDate, 'YYYYMM') as TimeID, S.StoreID, T.Avg_Stars as StarID, sum(D.Quantity) as Num_of_Books, sum(D.TotalPrice) as Total_Salesfrom TempBookWithAvgStar T, Sales S, SalesDetails Dwhere T.ISBN = D.ISBN and S.SalesID = D.SalesIDgroup by T.CategoryID, to_char(S.SalesDate, 'YYYYMM'), S.StoreID, T.Avg_Stars;
π‘ Common Mistake:Joining Book straight to Review inside the sales fact β a book with 4 reviews appears 4 times, so Num_of_Books is inflated 4Γ; the two temp tables exist to force one row per book before the sales join β Data Exploration (Warehouse Validation).
π‘ Common Mistake:Inner-joining Book and Review β silently deletes every unreviewed book from the sales figures; (+)+nvl(R.Stars, 0) is what preserves them.
πΉ Lab 7 β the five reports off a two-fact star
each question is routed to the fact that owns its measure
-- 1. total sales per bookstore per month -> BookSalesFactselect S.StoreID, T.Month, sum(F.Total_Sales) as Total_Salesfrom StoreDim S, TimeDim T, BookSalesFact Fwhere S.StoreID = F.StoreID and F.TimeID = T.TimeIDgroup by S.StoreID, T.Month order by S.StoreID, T.Month;-- 2. books sold per category -> BookSalesFact + shared CategoryDimselect C.CategoryID, C.CategoryDescription, sum(F.Num_of_Books) as Total_Num_Booksfrom BookSalesFact F, CategoryDim Cwhere F.CategoryID = C.CategoryIDgroup by C.CategoryID, C.CategoryDescription;-- 3. the single best-selling category -> top-N over an ordered inline viewselect * from ( select C.CategoryID, C.CategoryDescription, sum(F.Num_of_Books) as Total_Num_Books from BookSalesFact F, CategoryDim C where F.CategoryID = C.CategoryID group by C.CategoryID, C.CategoryDescription order by Total_Num_Books desc)where rownum = 1;-- 4. reviews per category -> ReviewFact, same CategoryDimselect C.CategoryID, C.CategoryDescription, sum(F.Num_of_Review) as Number_of_Reviewsfrom ReviewFact F, CategoryDim Cwhere F.CategoryID = C.CategoryIDgroup by C.CategoryID, C.CategoryDescription;-- 5. 5-star reviews per category -> ReviewFact + both shared dimensionsselect C.CategoryID, C.CategoryDescription, sum(F.Num_of_Review) as Number_of_5Star_Reviewsfrom ReviewFact F, CategoryDim C, StarRatingDim Swhere F.CategoryID = C.CategoryID and S.StarID = F.StarID and S.StarID = 5group by C.CategoryID, C.CategoryDescription;
π‘ Common Mistake:Reaching for the wrong fact β Num_of_Books exists only in BookSalesFact and Num_of_Review only in ReviewFact; no query in a multi-fact star joins the two facts to each other β they meet only through the shared dimensions.
π‘ Common Mistake:Writing where rownum = 1 β¦ order by β¦ in one block β Oracle assigns rownumbefore the sort, so it returns an arbitrary row; the order by must sit inside the inline view and rownum outside it.
πΉ Removing a dimension by pivoting the small fact
ReviewFACT2 β zero columns from the dimension, then a correlated update
create table ReviewFact2 as -- grid = one row per category, zeroedselect CategoryID, 0 as Num_of_0Star_Reviews, 0 as Num_of_1Star_Reviews, 0 as Num_of_2Star_Reviews, 0 as Num_of_3Star_Reviews, 0 as Num_of_4Star_Reviews, 0 as Num_of_5Star_Reviewsfrom CategoryDim;update ReviewFact2 F2 set Num_of_1Star_Reviews = nvl((select Num_of_Reviews from ReviewFact F1 where F2.CategoryID = F1.CategoryID and F1.StarID = 1), 0), Num_of_5Star_Reviews = nvl((select Num_of_Reviews from ReviewFact F1 where F2.CategoryID = F1.CategoryID and F1.StarID = 5), 0);
π‘ Common Mistake:Dropping the nvl β a category with no 3-star review returns NULL from the subquery and overwrites the initialised 0 with NULL β Pivoted Fact Tables.
βοΈ Core Decision Matrix
Situation
Dimension sets
Correct shape
Why
Book Sales vs Reviews
{S,T,C,R} vs {C,R}
two facts
a measure is meaningless against a dimension it does not apply to
Private Taxi: km, fuel, income
{Car,Driver,Week} for all three
one fact, three measures
identical dimension sets β one subject; units of measure are irrelevant
Car Service: service vs part
{T,B,Service,M} vs {T,B,Part,M}
two facts
different granularity; the finer fact needs a determinant PartDIM
When It Flips: three separate facts collapse into one the moment their dimension lists are found to be identical β the Private Taxi three-fact draft is unreasonable, not merely inefficient.
π Exam Execution Trace & Applied Exercises
Manual Execution Trace β the Book Sales applicability grid
Dimension
Total_Sales
Num_of_Books
Num_of_Reviews
Verdict
StoreDIM
β sales per store
β books sold per store
β ratings attach to books, not shops
sales fact only
TimeDIM
β sales per month
β books per month
β the stored date is the purchase date
sales fact only
CategoryDIM
β
β
β reviews per category
both facts
StarRatingDIM
β sales of 5-star books
β
β 5-star reviews per category
both facts
Reading the grid β the rows with a β are what split the schema; the two all-β dimensions are the shared ones and are physically created once.
Applied Exercise β is this one subject or three?
Problem: the Private Taxi warehouse has CarDIM, DriverDIM, WeekDIM and the measures Total Kilometres, Total Fuel Used, Total Income. A draft proposes one fact per measure. Decide, then write the merge.
Final Extracted Output: one fact, three measures β PrivateTaxiFACT(CarNoβ,DriverNoβ,WeekNoββ,Total_Kilometers,Total_Fuel_Used,Total_Income); splitting it again later is a vertical slice, not a modelling decision.
β οΈ Common Mistakes
π‘ Using the unit of measure as the subject test β βdistance, volume and money are three subjectsβ is the trap the Private Taxi case is built to catch; only the dimension set identifies the subject.
π‘ Reading applicability off intuition instead of the E/R diagram β βreviews per monthβ sounds sensible and is wrong, because the operational schema stores no review date.
π‘ Carrying every measure down to the finer fact β Total_Service_Cost at Part grain has no defensible value; a measure only descends if the source can attribute it at that grain.
π§ Active Recall
Two candidate facts share every dimension but measure different things. Combine or keep separate β and what settles it?
Answer
Short answer: combine β identical dimension sets mean identical subject, so it is one fact with multiple measures.
Why:Subject-orientation is defined by the analysis axes β the dimensions are the questions the business asks; measures answering the same questions belong together. The unit of measure is a decoy β km, litres and dollars differ in scale, not in subject. The merge is mechanical β join the facts on the full composite key, k facts needing kβ1 conditions per key column.
Why does the Part-grain car-service fact need a determinant dimension when the Service-grain fact does not?
Answer
Short answer: because one part is used by many services, so Number_of_Services is only correct while PartNo is pinned in the query.
Why:Finer grain re-uses the coarser measure β the same service is recorded once per part it consumed, so summing over parts counts it repeatedly. The double count is silent β a query grouping only on Brand and Month runs cleanly and returns an inflated integer. Hence the dashed box β PartDIM must appear in the where or the group by of every retrieval β Determinant Dimensions.