OLAP Cumulative and Moving Aggregates

Context: FIT3003_MOC · a value computed from the current row and the rows before it in the query result · family map in OLAP (On-Line Analytical Processing) Problem it solves: year-to-date running totals and -month moving averages beside each period’s own total, in one query.

Quick Revision

  • 🎯 Trigger: “cumulative”, “year to date”, “running total” ➔ sum(sum(m)) over (order by … rows unbounded preceding); “moving / rolling average over periods” ➔ avg(sum(m)) over (order by … rows k-1 preceding).
  • ⚡ Key Constraint: the window counts rows of the query result, not months ➔ rows 2 preceding is a 3-month average only when the over is ordered by time and no month is missing; the first rows average over fewer values.

🔧 Minimal Working Example

select S.TimeID,
       sum(Total_Sales) as Total_Sales,
       sum(sum(Total_Sales)) over (order by S.TimeID rows unbounded preceding) as Cumulative,
       avg(sum(Total_Sales)) over (order by S.TimeID rows 2 preceding)         as Avg_3_Months
from   SalesFact S, TimeDim T
where  S.TimeID = T.TimeID
and    Year = 2019
and    LocationID in ('MEL')
group by S.TimeID
order by S.TimeID;
  • Nested aggregate ➔ inner sum(Total_Sales) is the per-month group by total; the outer sum(…) over / avg(…) over runs across those monthly rows afterwards.
  • rows unbounded preceding ➔ window first row of the result current row.
  • rows n preceding ➔ window the rows behind the current one the current row, i.e. rows.

Expected output — MEL 2019 (two slide typos corrected: April is , the value the slide’s own cumulative implies, not the printed ; September’s cumulative is , not ):

TimeIDTotal_SalesCumulativeAvg_3_Monthswindow averaged
201901243,080243,080243,080Jan only
201902259,353502,433251,217Jan–Feb ( rows)
201903246,368748,801249,600Jan–Mar
201904237,913986,714247,878Feb–Apr
201905234,5131,221,227239,598Mar–May
201906235,6301,456,857236,019Apr–Jun
201907251,3301,708,187240,491May–Jul
201908239,5751,947,762242,178Jun–Aug
201909208,1652,155,927233,023Jul–Sep
201910228,7102,384,637225,483Aug–Oct
201911207,6942,592,331214,856Sep–Nov
201912235,9872,828,318224,130Oct–Dec

🔀 Variations

Restart per group — partition by

select LocationID, S.TimeID,
       to_char(sum(Total_Sales), '999,999,999') as Total_Sales,
       to_char(sum(sum(Total_Sales)) over
                 (partition by LocationID
                  order by LocationID, S.TimeID
                  rows unbounded preceding), '999,999,999') as Cumulative
from   SalesFact S, TimeDim T
where  S.TimeID = T.TimeID
and    Year = 2019
and    LocationID in ('MEL', 'PER')
group by LocationID, S.TimeID
order by LocationID, S.TimeID;
  • Reset ➔ without partition by, PER’s January would continue from MEL’s ; with it, row 13 restarts at PER’s own and ends at .
  • to_char(…, '999,999,999') ➔ display-only comma formatting; the column becomes a string.

Two running totals, two partitions — Lab 9a C.4

select t.time_year, f.mod_code,
       sum(f.tot_fuel) as Total,
       sum(sum(f.tot_fuel)) over (partition by t.time_year order by f.mod_code
                                  rows unbounded preceding) as Cum_Fuel_Year,
       sum(sum(f.tot_fuel)) over (partition by f.mod_code order by t.time_year
                                  rows unbounded preceding) as Cum_Fuel_Model
from   dw.charter_fact f, dw.time t
where  f.time_id = t.time_id
group by t.time_year, f.mod_code
order by t.time_year, f.mod_code;

Expected output (first rows of years models):

time_yearmod_codeTotalCum_Fuel_YearCum_Fuel_Model
1994C-90A16933.216,933.2016,933.20
1994PA23-2509086.926,020.109,086.90
1994PA31-35023773.849,793.9023,773.80
1995C-90A19058.819,058.8035,992.00
1995PA23-2509133.928,192.7018,220.80
  • Each over keeps its own partition ➔ Cum_Fuel_Year restarts every year and runs across models; Cum_Fuel_Model restarts every model and runs across years.
  • Order by the other column ➔ the lab solution writes partition by t.time_year order by time_year (and … mod_code order by f.mod_code): every row in a partition ties on its own key, so the accumulation order is undefined and only happens to come out sensible.
  • Lab 9a C.1 / C.2 ➔ the MWE shape on dw.charter_fact for 1995 revenue: cumulative ; 3-month moving average (, the first full window).

✍️ Practice

⚠️ Common Mistakes

  • 💡 Slide 47’s group by Location, TimeID ➔ Location is not in the from list (ORA-00904) and TimeID exists in both SalesFact and TimeDim (ORA-00918); group on LocationID, S.TimeID, exactly what is selected.
  • 💡 sum(Total_Sales) over (…) without the inner sum ➔ in a grouped query the raw Total_Sales is not a group expression (ORA-00979); the window must wrap the aggregate.
  • 💡 Relying on over’s order for display ➔ the order by inside over fixes the window, not the output; add an outer order by or the running column can print out of sequence.
  • 💡 partition by X order by X ➔ ordering a window by its own partition key gives no order inside the partition, so a running total accumulates in arbitrary sequence; order by the column the total should run along (Lab 9a C.4).