π― Objective: build one star schema per source, then stack them β Vertical Stacking when the per-source facts are union-compatible (same dimensions, same measures) | Horizontal Stacking when each source owns different measures.
π¦ Core Components:vertical β union collapses rows downward | horizontal β zero-padded TempFacts +union+ outer sum push every sourceβs measures into one wider row.
β‘ Key Constraint: union demands union-compatible column lists, so a horizontal stack must project every measure column in every TempFact, filling the ones that source does not own with 0.
π How It Works
1. Why multi-input is possible at all
Same theme, different schemas β the three student clubs store members, activities and financial expenditure in entirely different E/R diagrams β one even lives in an Excel workbook β but all answer the same two questions.
Integration is a warehouse property β Integrated is the first of the four warehouse features, and it is what licenses one fact over heterogeneous sources β Data Warehouse.
Reconcile the measure definition first β each source computes TotalIncome differently, so agree the definition before any SQL: Orchestra=βTicketSales; Business=β(PriceΓTickets+PriceΓ(1βDiscount)ΓMemberRegistrations); Japanese=β(MemberPriceΓMemberTickets+NonMemberPriceΓNonMemberTickets).
Qualify the source tables β tables are addressed as "ClubName.Table", e.g. "OrchestraClub.Concert", so the same table name in two sources never collides.
2. Vertical Stacking β University Student Clubs
Shape β every source yields the same star: UniversityClubFACT(ClubIDβ,Yearββ,TotalExpenditure,TotalIncome), so the facts stack vertically β rows down.
Shared dimension, built by union β derive a per-source YearDim with select distinct to_char(<date>, 'YYYY'), then union the three and select distinct again; derived attributes (UniversityStartDate/EndDate) are added afterwards with alter table+update.
Small dimension, hand-built β ClubDIM has one row per club and is written with three insert statements, not derived β Building Dimension Tables.
Stamp the source identity β each per-club TempFact gets alter table β¦ add (ClubID integer) then update β¦ set ClubID = n; without it the union loses which club a row came from.
Mind the level of aggregation inside a source β Concert is coarser than Ticket, so OrchestraTempFact1 is at ticket grain and needs a second group by before the final fact β Levels of Aggregation.
3. Horizontal Stacking β Real-Estate Property
Shape β PropertyFACT(Postcodeβ,MonthIDββ,TotalPropertiesAuction,TotalSuccessfulAuction,NumNewListing,TotalPropertiesSold,TotalSoldPrice) β five measures, each owned by a different source, so the facts stack horizontally β columns across.
Independent sources β Inspection DB (listings and inspections) Β· Auction DB (auctions and results) Β· Sold Property DB (completed sales); the same PROPERTY table exists in all three with different attributes, and only the Inspection DB carries ListedDate.
Measure ownership β Auction DB β TotalPropertiesAuction and TotalSuccessfulAuction (filter AuctionResultCode = 'SA') Β· Inspection DB β NumNewListing (count grouped on ListedDate) Β· Sold DB β TotalPropertiesSold and TotalSoldPrice.
Derived indicators are not stored β Auction Clearance Rate =TotalPropertiesAuctionTotalSuccessfulAuctionβ and mean price =TotalPropertiesSoldTotalSoldPriceβ are computed at query time β Fact Measure Aggregation Rules.
Shared dimensions come from all sources β MonthDIM unions ListedDate, AuctionDate and SoldDate; SuburbDIM unions Postcode, Suburb, City, State from all three PROPERTY tables, with select distinct wrapping the union to dedupe.
βοΈ Core Implementation
πΉ Vertical β one star per source, then one union
per-source fact β stamp ClubID β stack
create table OrchestraTempFact1 as -- ticket grain: one row per ticket soldselect T.ConcertID, to_char(CT.ConcertDate, 'YYYY') as Year, CT.HostingCost, CR.Honorarium, TT.Pricefrom "OrchestraClub.Ticket" T, "OrchestraClub.TicketType" TT, "OrchestraClub.Concert" CT, "OrchestraClub.Conductor" CRwhere T.TicketTypeID = TT.TicketTypeIDand CT.ConcertID = T.ConcertIDand CT.ConductorID = CR.ConductorID;create table OrchestraTempFact2 as -- concert grain: collapse the ticketsselect ConcertID, Year, HostingCost, Honorarium, sum(Price) as Incomefrom OrchestraTempFact1group by ConcertID, Year, HostingCost, Honorarium;alter table OrchestraTempFact2 add (ClubID integer);update OrchestraTempFact2 set ClubID = 1;create table OrchestraFact as -- club-year grain: the star's factselect ClubID, Year, sum(HostingCost + Honorarium) as TotalExpenditure, sum(Income) as TotalIncomefrom OrchestraTempFact2group by ClubID, Year;create table MonashClubFact as -- VERTICAL STACKselect * from OrchestraFactunionselect * from BusinessCommerceFactunionselect * from JapaneseFact;
π‘ Common Mistake:Skipping OrchestraTempFact2 β HostingCost and Honorarium are recorded per concert but TempFact1 is per ticket, so summing directly multiplies the expenditure by the number of tickets sold β Data Exploration (Warehouse Validation).
πΉ Horizontal β zero-pad every measure, union, then sum
one TempFact per measure-owning source, all with identical column lists
create table PropertyTempFact1 as -- Auction DB owns TotalPropertiesAuctionselect P.Postcode, to_char(A.AuctionDate, 'YYYYMM') as MonthID, count(*) as TotalPropertiesAuction, 0 as TotalSuccessfulAuction, 0 as NumNewListing, 0 as TotalPropertiesSold, 0 as TotalSoldPricefrom "Auction.Auction" A, "Auction.Property" Pwhere A.AuctionNo = P.AuctionNogroup by P.Postcode, to_char(A.AuctionDate, 'YYYYMM');create table PropertyTempFact3 as -- Inspection DB owns NumNewListingselect P.Postcode, to_char(P.ListedDate, 'YYYYMM') as MonthID, 0 as TotalPropertiesAuction, 0 as TotalSuccessfulAuction, count(*) as NumNewListing, 0 as TotalPropertiesSold, 0 as TotalSoldPricefrom "Inspection.Property" Pgroup by P.Postcode, to_char(P.ListedDate, 'YYYYMM');create table PropertyFact as -- HORIZONTAL STACKselect Postcode, MonthID, sum(TotalPropertiesAuction), sum(TotalSuccessfulAuction), sum(NumNewListing), sum(TotalPropertiesSold), sum(TotalSoldPrice)from ( select * from PropertyTempFact1 union select * from PropertyTempFact2 union select * from PropertyTempFact3 union select * from PropertyTempFact4 )group by Postcode, MonthID;
π‘ Common Mistake:union without the outer group by β¦ sum(β¦) β the union alone leaves up to four rows per (Postcode,MonthID), each carrying one real measure and four zeroes; the outer aggregation is what collapses them into one complete row.
πΉ Dimensions spanning all sources
select distinctaround the union, not just inside it
create table SuburbDim asselect distinct * from ( select distinct Postcode, Suburb, City, State from "Inspection.Property" union select distinct Postcode, Suburb, City, State from "Auction.Property" union select distinct Postcode, Suburb, City, State from "SoldProperty.Property");
βοΈ Core Decision Matrix
Method
Trigger condition
SQL shape
Failure mode if misapplied
Vertical Stacking
every source produces the same dimensions and the same measures
union of the per-source facts; a source-identifying dimension (ClubID) distinguishes the rows
none needed β but omitting the source key makes the stacked rows indistinguishable
Horizontal Stacking
each source owns different measures over shared dimensions
every TempFact projects all measures, padding non-owned ones with 0; union then outer group by+sum
dropping the padding breaks union compatibility; dropping the outer sum leaves fragmented rows
When It Flips: the test is whether a source contributes new rows or new columns to the integrated fact β rows β vertical, columns β horizontal.
β οΈ Common Mistakes
π‘ Unioning raw source tables instead of per-source facts β the sources are structurally different; integration happens after each has been reduced to the agreed dimensions and measures.
π‘ Letting each club keep its own income formula into the fact β the measures must be reconciled to one definition first, or the stacked fact adds incomparable numbers.
π‘ Padding with null instead of 0 β sum ignores nulls in Oracle but the unionβs type inference and any later arithmetic will not; the chapter pads with 0 throughout.
π‘ Forgetting that a source may hold an attribute the others lack β only the Inspection DB stores ListedDate, so NumNewListing can be sourced from nowhere else.
π§ Active Recall
Three source databases feed one fact. When do you union the facts, and when is union alone not enough?
Answer
Short answer:union alone works for vertical stacking; horizontal stacking needs zero-padded columns plus an outer group by β¦ sum(β¦).
Why:Vertical means same columns, new rows β each clubβs fact already has (ClubID,Year,TotalExpenditure,TotalIncome), so stacking is literally row concatenation. Horizontal means same rows, new columns β each real-estate source owns a different measure, and union demands union-compatible column lists, so every TempFact must project all five measures with 0 in the slots it does not own. The zeroes then have to be merged β four partial rows for one (Postcode,MonthID) collapse into one complete row only under sum, which is why the outer aggregation is not optional.
Why does the Orchestra club need two TempFacts when the other two clubs' processing looks flatter?
Answer
Short answer: because Concert sits at a higher level of aggregation than Ticket, so the first TempFact is at ticket grain.
Why:Income is per ticket, cost is per concert β joining Ticket to Concert repeats HostingCost and Honorarium once per ticket sold. The second TempFact re-grains β group by ConcertID, Year, HostingCost, Honorarium with sum(Price) collapses tickets to one row per concert, leaving each cost counted once. Only then may the club-year fact sum β the final group by ClubID, Year is safe because every input row is now a distinct concert β Levels of Aggregation.