π― Objective: a stored measure must survive re-aggregation β the analyst re-sums fact rows to roll up, so only measures whose roll-up equals the operational answer may be stored.
π¦ Core Components:count/sum β always safe | min/max β safe but not mixable | avg β never store.
β‘ Key Constraint: an average of averages is not the average β replace Average_X with Total_XandNumber_of_Y, and divide at query time.
π How It Works
1. Count semantics β pick the right one
count(*) β number of records in the group, NULLs included.
count(attribute) β excludes NULL values β the measure that makes an outer-joined fact correct.
count(distinct attribute) β removes duplication; Total Apps needs count(distinct ApplicationID) because one application appears once per download row.
2. Sum
Always re-aggregable β β of group sums = the global sum, so a stored sum rolls up correctly to any coarser grain.
Count and Sum are the common measures β the chapterβs default expectation for a fact table.
3. Average β the trap
Roll-up breaks β avg of per-group averages ignores group sizes, so it equals the true average only when all groups are the same size.
Correction is decomposition β store Total_Score and Number_of_Students; recover the average with sum(Total_Score)/sum(Number_of_Students).
4. Min / Max
Global value guaranteed β max of group maxima = the global maximum, likewise min; both may be stored.
Never mix them β max(Min_Score) or min(Max_Score) is a meaningless quantity.
βοΈ Core Implementation
πΉ Average of an average β the failing fact vs its correction
Fact v1 (stores Average_Score) vs Fact v2 (stores totals)
π‘ Common Mistake:Weighting is what is lost β semester 1 has 6 students and semester 2 has 2; averaging the two averages silently weights them equally.
πΉ Min / Max stored safely
Fact v3 ( Min_Score, Max_Score) queried globally
select max(Max_Score) from EnrolmentFact3 where UnitCode = 'IT001'; -- 87 βselect min(Min_Score) from EnrolmentFact3 where UnitCode = 'IT001'; -- 32 β
π‘ Common Mistake:max(Min_Score) answers nothing β βthe largest of the per-semester minimumsβ is not a business quantity.
βοΈ Core Decision Matrix
Aggregate
Stored as a measure?
Roll-up behaviour
Recovery at query time
count(*) / count(col) / count(distinct col)
β
additive
sum() over fact rows
sum
β
additive
sum() over fact rows
min / max
β (separately)
semi-additive β global value survives
min() / max(), never crossed
avg
β
not additive β weights are lost
store total + count, then sum/sum
When It Flips: an average is safe to display and unsafe to store. The rule fires the moment a fact row can be combined with another fact row β which is always, because that is what a dimension roll-up does.
π Exam Execution Trace & Applied Exercises
Manual Execution Trace β IT001 βDatabaseβ, two semesters
Step
Source
Value
Running interpretation
0
operational rows, sem 1
6 scores summing to 443
true group total
1
operational rows, sem 2
2 scores summing to 96
true group total
2
fact v1 stores averages
443/6=73.833, 96/2=48
sizes discarded
3
roll up fact v1
(73.833+48)/2=60.9165
β wrong by 6.46
4
roll up fact v2
(443+96)/(6+2)
β =67.375
Applied Exercise
Problem: the FitnessCentreFACT stores Num_of_Employees and Total_Salary by JobTitle, Month, EmploymentType, Gender. Why not Average_Salary?
Final Extracted Output:Total_Salary + Num_of_Employees is strictly more informative than Average_Salary β the average is recoverable from them, they are not recoverable from it.
π§ Active Recall
A fact row already summarises many transactions. Why does storing avg break while storing sum does not?
Answer
Short answer:sum is additive across groups; avg is not, because it divides by a group size the fact no longer stores.
Why:Roll-up is re-aggregation β every OLAP roll-up combines fact rows, so a measure is only valid if the operation applied to the stored values reproduces the operational answer. Sum composes β βgββSgβ=βS. Average loses the weights β once ngβ is discarded, k1ββgβavg(Sgβ) can only coincide with the true mean when every ngβ is equal.
After a left outer join in the TempFact, why is count(attribute) correct where count(*) is not?
Answer
Short answer: the outer join pads unmatched rows with NULLs, and count(attribute) excludes NULLs while count(*) counts the padded row.
Why:Two populations, one row set β every opening survives the join but only placed openings carry a CandNo. count(OpenNo) vs count(CandNo) β the same grouped rows then yield TotalOpeningβ₯TotalPlacement; count(*) would report both as the row count and erase the difference the analysis exists to measure β Building Fact Tables.