Learnitweb

Cumulative Spend per Customer per Month in Oracle

1. Introduction

In many analytical and financial systems, you often need to calculate how much a customer has spent over time — either within a month (running total) or across months (year-to-date cumulative spend).

For example:

  • For each customer, calculate their total spend for each month.
  • Then compute the running (cumulative) spend up to that month.

This is a classic window function (analytic function) use case in Oracle.


2. Example Table

Let’s start with a simple dataset.

CREATE TABLE customer_spends (
    cust_id   NUMBER,
    spend_date DATE,
    amount     NUMBER
);

INSERT INTO customer_spends VALUES (101, TO_DATE('2025-01-05','YYYY-MM-DD'), 200);
INSERT INTO customer_spends VALUES (101, TO_DATE('2025-01-15','YYYY-MM-DD'), 300);
INSERT INTO customer_spends VALUES (101, TO_DATE('2025-02-03','YYYY-MM-DD'), 150);
INSERT INTO customer_spends VALUES (101, TO_DATE('2025-02-20','YYYY-MM-DD'), 350);
INSERT INTO customer_spends VALUES (101, TO_DATE('2025-03-10','YYYY-MM-DD'), 400);

INSERT INTO customer_spends VALUES (102, TO_DATE('2025-01-08','YYYY-MM-DD'), 500);
INSERT INTO customer_spends VALUES (102, TO_DATE('2025-02-12','YYYY-MM-DD'), 200);
INSERT INTO customer_spends VALUES (102, TO_DATE('2025-02-28','YYYY-MM-DD'), 300);
INSERT INTO customer_spends VALUES (102, TO_DATE('2025-03-05','YYYY-MM-DD'), 100);
COMMIT;

3. Monthly Spend per Customer

First, we compute monthly totals per customer using the TRUNC(date, 'MM') function to group by month.

SELECT
    cust_id,
    TRUNC(spend_date, 'MM') AS month_start,
    SUM(amount) AS monthly_spend
FROM customer_spends
GROUP BY cust_id, TRUNC(spend_date, 'MM')
ORDER BY cust_id, month_start;

Output:

CUST_IDMONTH_STARTMONTHLY_SPEND
10101-JAN-25500
10101-FEB-25500
10101-MAR-25400
10201-JAN-25500
10201-FEB-25500
10201-MAR-25100

This gives the total spend per customer per month.


4. Cumulative Spend per Customer (Across Months)

To calculate cumulative spend across months (e.g., year-to-date total), we can apply the analytic SUM() function with an appropriate window.

WITH monthly_totals AS (
    SELECT
        cust_id,
        TRUNC(spend_date, 'MM') AS month_start,
        SUM(amount) AS monthly_spend
    FROM customer_spends
    GROUP BY cust_id, TRUNC(spend_date, 'MM')
)
SELECT
    cust_id,
    month_start,
    monthly_spend,
    SUM(monthly_spend) OVER (
        PARTITION BY cust_id
        ORDER BY month_start
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_spend
FROM monthly_totals
ORDER BY cust_id, month_start;

Output:

CUST_IDMONTH_STARTMONTHLY_SPENDCUMULATIVE_SPEND
10101-JAN-25500500
10101-FEB-255001000
10101-MAR-254001400
10201-JAN-25500500
10201-FEB-255001000
10201-MAR-251001100

Explanation:

  • The SUM() OVER() clause calculates a running total for each customer.
  • The window ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ensures that Oracle accumulates values from the first month to the current month.

5. Cumulative Spend per Customer per Month (Within the Month)

Sometimes, you want to calculate running totals within the same month — for example, to see how a customer’s spend increased throughout that month.

SELECT
    cust_id,
    TRUNC(spend_date, 'MM') AS month_start,
    spend_date,
    amount,
    SUM(amount) OVER (
        PARTITION BY cust_id, TRUNC(spend_date, 'MM')
        ORDER BY spend_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_monthly_spend
FROM customer_spends
ORDER BY cust_id, spend_date;

Output:

CUST_IDMONTH_STARTSPEND_DATEAMOUNTCUMULATIVE_MONTHLY_SPEND
10101-JAN-2505-JAN-25200200
10101-JAN-2515-JAN-25300500
10101-FEB-2503-FEB-25150150
10101-FEB-2520-FEB-25350500
10101-MAR-2510-MAR-25400400
10201-JAN-2508-JAN-25500500
10201-FEB-2512-FEB-25200200
10201-FEB-2528-FEB-25300500
10201-MAR-2505-MAR-25100100

Explanation:

  • The query calculates cumulative spend within each month per customer.
  • The PARTITION BY cust_id, TRUNC(spend_date, 'MM') restarts the running total every month.

6. Cumulative Spend Across the Entire Year (Rolling Total)

To calculate a year-to-date cumulative spend, even when data spans multiple years, we can extend the logic by truncating to year and month together.

WITH monthly_totals AS (
    SELECT
        cust_id,
        TRUNC(spend_date, 'MM') AS month_start,
        SUM(amount) AS monthly_spend
    FROM customer_spends
    GROUP BY cust_id, TRUNC(spend_date, 'MM')
)
SELECT
    cust_id,
    month_start,
    monthly_spend,
    SUM(monthly_spend) OVER (
        PARTITION BY cust_id, TRUNC(month_start, 'YYYY')
        ORDER BY month_start
    ) AS ytd_cumulative_spend
FROM monthly_totals
ORDER BY cust_id, month_start;

Output (if data spans multiple years):

CUST_IDMONTH_STARTMONTHLY_SPENDYTD_CUMULATIVE_SPEND
10101-JAN-25500500
10101-FEB-255001000
10101-MAR-254001400

7. Handling Missing Months (Optional Enhancement)

If a customer has no spend in some months, you might still want to show the month with zero spend.

To do that, generate all months between the first and last transaction using a recursive CTE or CONNECT BY, and then left join to your totals.

WITH month_list AS (
    SELECT ADD_MONTHS(TRUNC(MIN(spend_date), 'MM'), LEVEL - 1) AS month_start
    FROM customer_spends
    CONNECT BY LEVEL <= MONTHS_BETWEEN(MAX(spend_date), MIN(spend_date)) + 1
),
cust_months AS (
    SELECT DISTINCT cust_id FROM customer_spends
),
all_combinations AS (
    SELECT c.cust_id, m.month_start
    FROM cust_months c
    CROSS JOIN month_list m
),
monthly_totals AS (
    SELECT
        cust_id,
        TRUNC(spend_date, 'MM') AS month_start,
        SUM(amount) AS monthly_spend
    FROM customer_spends
    GROUP BY cust_id, TRUNC(spend_date, 'MM')
)
SELECT
    a.cust_id,
    a.month_start,
    NVL(mt.monthly_spend, 0) AS monthly_spend,
    SUM(NVL(mt.monthly_spend, 0)) OVER (
        PARTITION BY a.cust_id
        ORDER BY a.month_start
    ) AS cumulative_spend
FROM all_combinations a
LEFT JOIN monthly_totals mt
ON a.cust_id = mt.cust_id AND a.month_start = mt.month_start
ORDER BY a.cust_id, a.month_start;

This version:

  • Ensures every month is shown for every customer.
  • Fills missing months with 0.
  • Computes cumulative spend continuously, even for months with no activity.

8. Key Functions Used

FunctionPurpose
TRUNC(date, 'MM')Truncates date to the first day of the month
SUM() OVER (...)Computes running total using analytic window
PARTITION BYGroups cumulative total per customer
ORDER BY (inside window)Orders transactions by date/month for cumulative logic
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWDefines the cumulative window (optional in Oracle)
NVL()Handles nulls for missing data
CONNECT BY LEVELGenerates a sequence of months for filling missing data

9. Summary

ScenarioQuery ApproachDescription
Monthly total per customerGROUP BY TRUNC(date,'MM')Aggregate spend per month
Cumulative per month (within same month)SUM() OVER (PARTITION BY cust_id, month)Running total by date
Cumulative across monthsSUM() OVER (PARTITION BY cust_id ORDER BY month)Running total by month
Show missing monthsGenerate sequence using CONNECT BYFill missing months with 0 spend

10. Conclusion

In Oracle, analytic window functions make cumulative calculations simple and efficient.
For cumulative spend per customer per month:

  1. Aggregate data by month.
  2. Use SUM() OVER (PARTITION BY cust_id ORDER BY month) to calculate the cumulative running total.
  3. Optionally, handle missing months using CONNECT BY or recursive CTE.