Building Bridge Table Schemas

Context: FIT3003_MOC Β· the SQL half of Bridge Tables Β· same build order as Building Dimension Tables βž” Building Fact Tables, with the bridge inserted between two dimensions Problem it solves: turning an operational associative table into a bridge, and computing the two optional columns (, ) that live in the parent dimension.

Quick Revision

  • 🎯 Trigger: the source has an associative table with exactly two FKs and the far entity cannot key the fact βž” create table XxxBridge as select * from <associative table>;
  • ⚑ Key Constraint: and are computed by grouping the parent against the associative table β€” never against the bridge you just built, and never against the fact.

πŸ”§ Minimal Working Example

(Product Sales, Chapter 5 β€” the whole snowflake in one script; the bridge is a straight copy of )

-- Time Dimension
create table TimeDim as
select
   distinct to_char(SalesDate, 'YYYYMM') as TimeID,
   to_char(SalesDate, 'YYYY') as Year,
   to_char(SalesDate, 'MM') as Month
from Sales;
 
-- Customer Location Dimension
create table CustLocDim as
select distinct Suburb, Postcode
from Customer;
 
-- Product Dimension
create table ProductDim as
select distinct ProductNo, ProductName
from Product;
 
-- Bridge Table
create table ProductSupplierBridge as
select *
from StockSupplier;
 
-- Supplier Dimension
create table SupplierDim as
Select SupplierID, Name as SupplierName
from Supplier;
 
-- Fact Table
create table ProductSalesFact as
Select
   to_char(S.SalesDate, 'YYYYMM') as TimeID,
   P.ProductNo,
   C.Suburb,
   sum(SI.QtySold*P.Price) as TotalSales
from Sales S, Product P, Customer C, SalesItem SI
where S.SalesNo = SI.SalesNo
and SI.ProductNo= P.ProductNo
and C.CustomerID = S.CustomerID
group by
   to_char(S.SalesDate, 'YYYYMM'),
   P.ProductNo,
   C.Suburb;

Expected output: with no β€” supplier is reachable only via . Four tables in the fact’s from ⟹ three join conditions.

πŸ”€ Variations

Model 1 β€” bridge only (Lab 4, Truck Delivery)

Build order: (copy of ) Β· (by hand, four rows: Summer, Autumn, Winter, Spring) Β· (projection of ) Β· (copy of ) Β· (copy of ) Β· a TempFact with the season alter/update, then βž” Building Fact Tables Route B.

  • The bridge is the cheapest table in the script βž” already is the bridge; copy it, drop nothing.

Model 2 β€” add the weight factor to

Create Table TripDim2 As
select t.tripid, t.tripdate, t.totalkm, 1.0/count(*) as weightfactor
from trip t, destination d
where t.tripid = d.tripid
group by t.tripid, t.tripdate, t.totalkm;

Expected output:

TRIPIDTRIPDATETOTALKMWEIGHTFACTOR
Trip114-APR-133700.2
Trip214-APR-135700.333333
Trip314-APR-132500.333333
Trip415-JUL-134500.25
Trip515-JUL-131750.5
  • 1.0/count(*) not 1/count(*) βž” integer division would floor every weight to ; the forces the numeric type.
  • Inner join drops empty trips βž” a trip with no row in never appears, so it silently loses its weight; check for one before trusting the row count βž” Data Exploration (Warehouse Validation).
  • Then use it: total cost per store across the bridge.
Select S.StoreId, S.StoreName,
   sum(Total_delivery_Cost * Weight_Factor) as "Total Cost for Store"
from
  TruckFact2 F, TripDim2 T,
  StoreDim2 S, BridgeTableDim2 B
where F.TripId = T.TripId
and   T.TripId = B.TripId
and   B.StoreId = S.StoreId
group by S.StoreId, S.StoreName
order by S.StoreId, S.StoreName;

Model 3 β€” add the list aggregate

Create Table TripDim3 As
Select T.TripID, T.TripDate, T.TotalKm, 1.0/count(D.StoreID) As WeightFactor,
   LISTAGG (D.StoreID, '_') Within Group (Order By D.StoreID) As StoreGroupList
From Trip T, Destination D
Where T.TripID = D.TripID
Group By T.TripID, T.TripDate, T.TotalKm;

Expected output:

TRIPIDTRIPDATETOTALKMWEIGHTFACTORSTOREGROUPLIST
Trip114-APR-133700.2M1_M2_M3_M4_M8
Trip214-APR-135700.333333M1_M2_M4
Trip314-APR-132500.333333M1_M5_M6
Trip415-JUL-134500.25M5_M6_M7_M8
Trip515-JUL-131750.5M3_M7
  • Syntax βž” LISTAGG(attr1, '_') Within Group (Order By attr1) As columnname; append Desc inside Order By to reverse the list.
  • Within Group is mandatory βž” LISTAGG is an ordered-set aggregate, so the sort key is declared separately from the group by.

Joining with vs without

-- With StoreGroupList: two tables, a LIKE against the concatenated list
select *
from TripDim3 T, StoreDim3 S
where T.StoreGroupList like '%'||S.StoreID||'%';
 
-- Without StoreGroupList: three tables, an equi-join through the bridge
select *
from TripDim3 T, BridgeTable3 B, StoreDim3 S
where T.TripID = B.TripID
and B.StoreID = S.StoreID;
  • Trade βž” the list version is visually appealing and needs one join fewer; it is redundant storage and the LIKE cannot use an index.

✍️ Practice

⚠️ Common Mistakes

  • πŸ’‘ 1/count(*) βž” integer division returns for every trip with more than one store; write 1.0/count(*).
  • πŸ’‘ Grouping on the bridge instead of βž” the bridge is a copy, so it works here, but the pattern is β€œaggregate from the operational source” βž” Building Fact Tables.
  • πŸ’‘ Omitting Within Group from LISTAGG βž” Oracle rejects the call; the ordering clause is part of the function, not optional decoration.
  • πŸ’‘ Forgetting the join condition in a comma-list from βž” Cartesian product, quota blown, account locked βž” SQL Joins (ANSI).