🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Running Totals & Cumulative — Window Frames, Moving Averages, Cumulative Distribution

Advanced SQLWindow Functions⭐ Premium

Advertisement

Running Totals & Cumulative

Advanced SQL

Cumulative Calculations in SQL

Running totals and cumulative calculations answer: "What's the total so far?" and "How does each row compare to all previous rows?" These are essential for financial reports, trend analysis, and cumulative metrics.

  • Running TotalSUM() OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
  • Moving AverageAVG() OVER (ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
  • Cumulative % — Running total / grand total
  • CDFCUME_DIST() and PERCENT_RANK()

The key distinction is ROWS vs RANGE in the window frame clause — using the wrong one produces incorrect results when there are ties.


Window Frame Clause

Window Frame: ROWS BETWEEN 2 PRECEDING AND CURRENT ROWJan 1: 100Jan 2: 120Jan 3: 110Jan 4: 130Jan 5: 140Jan 6: 90Jan 7: 150Jan 8: 160Jan 9: 170Frame for Jan 5: 110 + 130 + 140 = 380MA(3): 110MA(3): 110MA(3): 110MA(3): 120MA(3): 127MA(3): 120MA(3): 127MA(3): 133MA(3): 140

Basic Running Total

-- Simple running total
SELECT
  sale_date,
  revenue,
  SUM(revenue) OVER (
    ORDER BY sale_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total
FROM daily_sales
ORDER BY sale_date;

ℹ️

Key Insight: Always use ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW for a true running total. Without the frame clause, PostgreSQL defaults to RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW which includes all peers (ties in ORDER BY).


Running Total by Partition

-- Running total per department (resets per dept)
SELECT
  department_id, employee_id, salary,
  SUM(salary) OVER (
    PARTITION BY department_id
    ORDER BY hire_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS dept_running_total,
  SUM(salary) OVER (
    ORDER BY hire_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS company_running_total
FROM employees
ORDER BY department_id, hire_date;

Cumulative Percentage

-- Calculate cumulative percentage of total
SELECT
  department_id, employee_id, salary,
  SUM(salary) OVER w AS running_total,
  SUM(salary) OVER () AS grand_total,
  ROUND(SUM(salary) OVER w * 100.0 / SUM(salary) OVER (), 2) AS cumulative_pct
FROM employees
WINDOW w AS (
  ORDER BY salary DESC
  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
ORDER BY salary DESC;

Running Average (Moving Averages)

-- Moving averages with different windows
SELECT
  sale_date, revenue,
  AVG(revenue) OVER (ORDER BY sale_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma_7day,
  AVG(revenue) OVER (ORDER BY sale_date ROWS BETWEEN 29 PRECEDING AND CURRENT ROW) AS ma_30day,
  AVG(revenue) OVER (ORDER BY sale_date ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING) AS ma_centered_7day
FROM daily_sales
ORDER BY sale_date;

Cumulative Sum with Reset at Boundaries

-- Running total that resets at month boundaries
SELECT
  department_id, employee_id, salary, hire_date,
  SUM(salary) OVER (
    PARTITION BY department_id
    ORDER BY hire_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS dept_running_total,
  -- Reset at month boundaries
  SUM(salary) OVER (
    PARTITION BY department_id, DATE_TRUNC('month', hire_date)
    ORDER BY hire_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS monthly_running_total
FROM employees
ORDER BY department_id, hire_date;

⚠️

Common Mistake: Using RANGE instead of ROWS in the frame clause can produce unexpected results when there are ties in the ORDER BY column. ROWS is generally safer for running totals — it always includes exactly the specified number of physical rows.


Running Maximum and Minimum

-- Running min/max with window functions
SELECT
  sale_date, revenue,
  MAX(revenue) OVER (ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_max,
  MIN(revenue) OVER (ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_min,
  revenue - MIN(revenue) OVER (ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS above_running_min,
  MAX(revenue) OVER (ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) - revenue AS below_running_max
FROM daily_sales
ORDER BY sale_date;

Cumulative Distribution Function

-- CDF and percentile rank calculations
SELECT
  employee_id, salary,
  CUME_DIST() OVER (ORDER BY salary) AS cumulative_distribution,
  PERCENT_RANK() OVER (ORDER BY salary) AS percentile_rank,
  NTILE(100) OVER (ORDER BY salary) AS percentile_bucket
FROM employees
ORDER BY salary;

Running Total with Gaps

-- Handle gaps in data with running total
WITH date_series AS (
  SELECT generate_series(
    MIN(sale_date), MAX(sale_date), '1 day'::INTERVAL
  )::DATE AS date
  FROM daily_sales
)
SELECT
  ds.date,
  COALESCE(s.revenue, 0) AS revenue,
  SUM(COALESCE(s.revenue, 0)) OVER (
    ORDER BY ds.date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total
FROM date_series ds
LEFT JOIN daily_sales s ON ds.date = s.sale_date
ORDER BY ds.date;

Quiz: Test Your Knowledge


Follow-Up Questions

  1. What's the difference between ROWS and RANGE in running total calculations?
  2. How would you calculate a running total that handles NULL values?
  3. Explain how to compute a running geometric mean.
  4. How do you reset running totals at specific intervals?
  5. What's the best approach for running totals in partitioned tables?

Key Takeaways

  • Always specify ROWSROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW for true running totals
  • PARTITION BY resets — running total starts fresh at each partition boundary
  • Moving averageROWS BETWEEN 6 PRECEDING AND CURRENT ROW for 7-day MA
  • Cumulative % — running total / grand total × 100
  • CUME_DIST() — returns the proportion of values ≤ current value
  • RANGE vs ROWS — RANGE includes ties; ROWS is exact row count
🔒

Premium Content

Running Totals & Cumulative — Window Frames, Moving Averages, Cumulative Distribution

You've previewed the first section. Unlock this full lesson and 900+ advanced tutorials with a Premium plan.

🎯End-to-end Projects
💼Interview Prep
📜Certificates
🤝Community Access

Already a member? Log in

Advertisement