Oracle SQL Query to Calculate Running Totals

Running totals, also known as cumulative sums, are essential for financial reporting, inventory tracking, and data analysis tasks where you need to see accumulated values over time.

This article explores various methods to calculate running totals in Oracle SQL, from traditional approaches to modern window functions, complete with practical examples and performance considerations.

What Is a Running Total and Why Is It Important?

A running total represents the cumulative sum of values up to and including the current row in a dataset.

Unlike simple aggregations that provide a single total, running totals show how values accumulate progressively, making them invaluable for trend analysis, balance calculations, and progressive reporting.

Table showing employee salaries and their running totals using Oracle SQL window functions.

Financial analysts use running totals to track account balances, inventory managers monitor stock levels, and sales teams analyze cumulative revenue throughout periods.

How Does the SUM() Window Function Calculate Running Totals?

Oracle's window functions provide the most elegant solution for calculating running totals using the SUM() function with an OVER clause.

Data Preparation

Let's create a sales table to demonstrate running total calculations:

-- Create the sales table
CREATE TABLE exrt_sales (
    sale_id NUMBER PRIMARY KEY,
    sale_date DATE,
    product_name VARCHAR2(50),
    amount NUMBER(10,2)
);

-- Insert sample data
INSERT INTO exrt_sales VALUES (1, DATE '2025-01-05', 'Laptop', 1200.00);
INSERT INTO exrt_sales VALUES (2, DATE '2025-01-08', 'Mouse', 45.00);
INSERT INTO exrt_sales VALUES (3, DATE '2025-01-10', 'Keyboard', 85.00);
INSERT INTO exrt_sales VALUES (4, DATE '2025-01-15', 'Monitor', 350.00);
INSERT INTO exrt_sales VALUES (5, DATE '2025-01-18', 'Laptop', 1150.00);
INSERT INTO exrt_sales VALUES (6, DATE '2025-01-20', 'Mouse', 40.00);
INSERT INTO exrt_sales VALUES (7, DATE '2025-01-22', 'Headphones', 120.00);
INSERT INTO exrt_sales VALUES (8, DATE '2025-01-25', 'Monitor', 380.00);

COMMIT;
-- Display the data
SELECT * FROM exrt_sales ORDER BY sale_date;
SALE_IDSALE_DATEPRODUCT_NAMEAMOUNT
105-JAN-25Laptop1200.00
208-JAN-25Mouse45.00
310-JAN-25Keyboard85.00
415-JAN-25Monitor350.00
518-JAN-25Laptop1150.00
620-JAN-25Mouse40.00
722-JAN-25Headphones120.00
825-JAN-25Monitor380.00

Basic Running Total Calculation

This query calculates a running total of sales amounts ordered by date:

SELECT 
    sale_id,
    sale_date,
    product_name,
    amount,
    SUM(amount) OVER (ORDER BY sale_date) AS running_total
FROM exrt_sales
ORDER BY sale_date;
SALE_IDSALE_DATEPRODUCT_NAMEAMOUNTRUNNING_TOTAL
105-JAN-25Laptop1200.001200.00
208-JAN-25Mouse45.001245.00
310-JAN-25Keyboard85.001330.00
415-JAN-25Monitor350.001680.00
518-JAN-25Laptop1150.002830.00
620-JAN-25Mouse40.002870.00
722-JAN-25Headphones120.002990.00
825-JAN-25Monitor380.003370.00

The OVER clause with ORDER BY creates a window frame that includes all rows from the beginning up to the current row.

What Are the Different Window Frame Options for Running Totals?

Window frames define which rows to include in the calculation, offering flexibility in how running totals are computed.

Understanding ROWS and RANGE Clauses

The default behavior uses RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, but you can explicitly specify the frame:

SELECT 
    sale_id,
    sale_date,
    amount,
    SUM(amount) OVER (
        ORDER BY sale_date 
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total_rows,
    SUM(amount) OVER (
        ORDER BY sale_date 
        RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total_range
FROM exrt_sales
ORDER BY sale_date;
SALE_IDSALE_DATEAMOUNTRUNNING_TOTAL_ROWSRUNNING_TOTAL_RANGE
105-JAN-251200.001200.001200.00
208-JAN-2545.001245.001245.00
310-JAN-2585.001330.001330.00
415-JAN-25350.001680.001680.00
518-JAN-251150.002830.002830.00
620-JAN-2540.002870.002870.00
722-JAN-25120.00120.002990.00
825-JAN-25380.003370.003370.00

ROWS considers physical row positions, while RANGE considers logical values, which matters when dealing with duplicate order values.

How Can You Calculate Running Totals with Partitions?

Partitioned running totals reset the calculation for each group, useful for category-wise or period-wise analysis.

Running Totals by Product Category

This query calculates separate running totals for each product:

SELECT 
    sale_id,
    sale_date,
    product_name,
    amount,
    SUM(amount) OVER (
        PARTITION BY product_name 
        ORDER BY sale_date
    ) AS product_running_total
FROM exrt_sales
ORDER BY product_name, sale_date;
SALE_IDSALE_DATEPRODUCT_NAMEAMOUNTPRODUCT_RUNNING_TOTAL
722-JAN-25Headphones120.00120.00
310-JAN-25Keyboard85.0085.00
105-JAN-25Laptop1200.001200.00
518-JAN-25Laptop1150.002350.00
415-JAN-25Monitor350.00350.00
825-JAN-25Monitor380.00730.00
208-JAN-25Mouse45.0045.00
620-JAN-25Mouse40.0085.00

Each product maintains its own running total, resetting when a new product category begins.

What Are Alternative Methods to Calculate Running Totals?

While window functions are preferred, understanding alternative approaches helps with legacy systems or specific requirements.

Using Self-Joins

Before window functions, self-joins were the primary method for calculating running totals:

SELECT 
    s1.sale_id,
    s1.sale_date,
    s1.product_name,
    s1.amount,
    SUM(s2.amount) AS running_total
FROM exrt_sales s1
JOIN exrt_sales s2 ON s2.sale_date <= s1.sale_date
GROUP BY s1.sale_id, s1.sale_date, s1.product_name, s1.amount
ORDER BY s1.sale_date;
SALE_IDSALE_DATEPRODUCT_NAMEAMOUNTRUNNING_TOTAL
105-JAN-25Laptop1200.001200.00
208-JAN-25Mouse45.001245.00
310-JAN-25Keyboard85.001330.00
415-JAN-25Monitor350.001680.00
518-JAN-25Laptop1150.002830.00
620-JAN-25Mouse40.002870.00
722-JAN-25Headphones120.002990.00
825-JAN-25Monitor380.003370.00

This method works but performs poorly on large datasets due to the exponential growth of join operations.

Using Correlated Subqueries

Another traditional approach uses correlated subqueries:

SELECT 
    sale_id,
    sale_date,
    product_name,
    amount,
    (SELECT SUM(amount) 
     FROM exrt_sales s2 
     WHERE s2.sale_date <= s1.sale_date) AS running_total
FROM exrt_sales s1
ORDER BY sale_date;
SALE_IDSALE_DATEPRODUCT_NAMEAMOUNTRUNNING_TOTAL
105-JAN-25Laptop1200.001200.00
208-JAN-25Mouse45.001245.00
310-JAN-25Keyboard85.001330.00
415-JAN-25Monitor350.001680.00
518-JAN-25Laptop1150.002830.00
620-JAN-25Mouse40.002870.00
722-JAN-25Headphones120.002990.00
825-JAN-25Monitor380.003370.00

Like self-joins, this method suffers from performance issues as the subquery executes for each row.

How Do You Handle Complex Running Total Scenarios?

Real-world applications often require more sophisticated running total calculations beyond simple cumulative sums.

Running Totals with Conditional Logic

Let's create an inventory table to demonstrate running totals with conditions:

-- Create inventory movements table
CREATE TABLE exrt_inventory (
    movement_id NUMBER PRIMARY KEY,
    movement_date DATE,
    product_name VARCHAR2(50),
    movement_type VARCHAR2(10),
    quantity NUMBER
);

-- Insert sample data
INSERT INTO exrt_inventory VALUES (1, DATE '2025-01-01', 'Widget', 'IN', 100);
INSERT INTO exrt_inventory VALUES (2, DATE '2025-01-05', 'Widget', 'OUT', 20);
INSERT INTO exrt_inventory VALUES (3, DATE '2025-01-08', 'Widget', 'IN', 50);
INSERT INTO exrt_inventory VALUES (4, DATE '2025-01-10', 'Widget', 'OUT', 30);
INSERT INTO exrt_inventory VALUES (5, DATE '2025-01-12', 'Widget', 'OUT', 25);
INSERT INTO exrt_inventory VALUES (6, DATE '2025-01-15', 'Widget', 'IN', 40);

COMMIT;
-- Display the data
SELECT * FROM exrt_inventory ORDER BY movement_date;
MOVEMENT_IDMOVEMENT_DATEPRODUCT_NAMEMOVEMENT_TYPEQUANTITY
101-JAN-25WidgetIN100
205-JAN-25WidgetOUT20
308-JAN-25WidgetIN50
410-JAN-25WidgetOUT30
512-JAN-25WidgetOUT25
615-JAN-25WidgetIN40

This query calculates running inventory balance considering inbound and outbound movements:

SELECT 
    movement_id,
    movement_date,
    movement_type,
    quantity,
    SUM(CASE 
        WHEN movement_type = 'IN' THEN quantity 
        WHEN movement_type = 'OUT' THEN -quantity 
    END) OVER (ORDER BY movement_date) AS running_balance
FROM exrt_inventory
ORDER BY movement_date;
MOVEMENT_IDMOVEMENT_DATEMOVEMENT_TYPEQUANTITYRUNNING_BALANCE
101-JAN-25IN100100
205-JAN-25OUT2080
308-JAN-25IN50130
410-JAN-25OUT30100
512-JAN-25OUT2575
615-JAN-25IN40115

Running Percentage Calculations

Running totals often combine with percentage calculations for deeper insights:

SELECT 
    sale_id,
    sale_date,
    product_name,
    amount,
    SUM(amount) OVER (ORDER BY sale_date) AS running_total,
    ROUND(
        100 * SUM(amount) OVER (ORDER BY sale_date) / 
        SUM(amount) OVER (), 2
    ) AS running_percentage
FROM exrt_sales
ORDER BY sale_date;
SALE_IDSALE_DATEPRODUCT_NAMEAMOUNTRUNNING_TOTALRUNNING_PERCENTAGE
105-JAN-25Laptop1200.001200.0035.61
208-JAN-25Mouse45.001245.0036.94
310-JAN-25Keyboard85.001330.0039.47
415-JAN-25Monitor350.001680.0049.85
518-JAN-25Laptop1150.002830.0083.98
620-JAN-25Mouse40.002870.0085.16
722-JAN-25Headphones120.002990.0088.72
825-JAN-25Monitor380.003370.00100.00

This calculation shows what percentage of total sales has been achieved at each point in time.

What Performance Considerations Apply to Running Total Queries?

Performance optimization becomes critical when dealing with large datasets or complex running total calculations.

Indexing Strategies

Creating appropriate indexes significantly improves running total query performance:

-- Create index on date column for time-based running totals
CREATE INDEX idx_exrt_sales_date ON exrt_sales(sale_date);

-- Create composite index for partitioned running totals
CREATE INDEX idx_exrt_sales_product_date ON exrt_sales(product_name, sale_date);

Indexes on ORDER BY and PARTITION BY columns enable Oracle to efficiently sort and group data for window function calculations.

Materialized Views for Frequent Calculations

For frequently accessed running totals, materialized views provide pre-calculated results:

CREATE MATERIALIZED VIEW exrt_sales_running_totals
BUILD IMMEDIATE
REFRESH COMPLETE ON DEMAND
AS
SELECT 
    sale_id,
    sale_date,
    product_name,
    amount,
    SUM(amount) OVER (ORDER BY sale_date) AS running_total,
    SUM(amount) OVER (PARTITION BY product_name ORDER BY sale_date) AS product_running_total
FROM exrt_sales;

This approach trades storage space for query performance, ideal for reporting scenarios where real-time updates aren't critical.

How Can You Apply Running Totals in Real-World Scenarios?

Understanding practical applications helps implement running totals effectively in business contexts.

Financial Balance Tracking

Let's create a bank account transactions table:

-- Create bank transactions table
CREATE TABLE exrt_bank_transactions (
    transaction_id NUMBER PRIMARY KEY,
    transaction_date DATE,
    description VARCHAR2(100),
    debit NUMBER(10,2),
    credit NUMBER(10,2)
);

-- Insert sample data
INSERT INTO exrt_bank_transactions VALUES (1, DATE '2025-01-01', 'Opening Balance', 0, 5000);
INSERT INTO exrt_bank_transactions VALUES (2, DATE '2025-01-05', 'Salary Deposit', 0, 3500);
INSERT INTO exrt_bank_transactions VALUES (3, DATE '2025-01-08', 'Rent Payment', 1200, 0);
INSERT INTO exrt_bank_transactions VALUES (4, DATE '2025-01-10', 'Grocery Shopping', 250, 0);
INSERT INTO exrt_bank_transactions VALUES (5, DATE '2025-01-15', 'Freelance Income', 0, 800);
INSERT INTO exrt_bank_transactions VALUES (6, DATE '2025-01-18', 'Utilities', 150, 0);
INSERT INTO exrt_bank_transactions VALUES (7, DATE '2025-01-20', 'Online Purchase', 85, 0);
INSERT INTO exrt_bank_transactions VALUES (8, DATE '2025-01-25', 'Interest Credit', 0, 25);

COMMIT;
-- Display the data
SELECT * FROM exrt_bank_transactions ORDER BY transaction_date;
TRANSACTION_IDTRANSACTION_DATEDESCRIPTIONDEBITCREDIT
101-JAN-25Opening Balance05000
205-JAN-25Salary Deposit03500
308-JAN-25Rent Payment12000
410-JAN-25Grocery Shopping2500
515-JAN-25Freelance Income0800
618-JAN-25Utilities1500
720-JAN-25Online Purchase850
825-JAN-25Interest Credit025

This query calculates the running balance for the bank account:

SELECT 
    transaction_id,
    transaction_date,
    description,
    debit,
    credit,
    credit - debit AS net_amount,
    SUM(credit - debit) OVER (ORDER BY transaction_date, transaction_id) AS balance
FROM exrt_bank_transactions
ORDER BY transaction_date, transaction_id;
TRANSACTION_IDTRANSACTION_DATEDESCRIPTIONDEBITCREDITNET_AMOUNTBALANCE
101-JAN-25Opening Balance0500050005000
205-JAN-25Salary Deposit0350035008500
308-JAN-25Rent Payment12000-12007300
410-JAN-25Grocery Shopping2500-2507050
515-JAN-25Freelance Income08008007850
618-JAN-25Utilities1500-1507700
720-JAN-25Online Purchase850-857615
825-JAN-25Interest Credit025257640

Sales Performance Analysis

Running totals help analyze sales performance against targets:

-- Create sales targets table
CREATE TABLE exrt_sales_targets (
    month_year VARCHAR2(7),
    target_amount NUMBER(10,2)
);

INSERT INTO exrt_sales_targets VALUES ('2025-01', 3000);

COMMIT;
-- Calculate running total with target comparison
WITH monthly_sales AS (
    SELECT 
        TO_CHAR(sale_date, 'YYYY-MM') AS month_year,
        sale_date,
        amount,
        SUM(amount) OVER (
            PARTITION BY TO_CHAR(sale_date, 'YYYY-MM') 
            ORDER BY sale_date
        ) AS month_running_total
    FROM exrt_sales
)
SELECT 
    ms.sale_date,
    ms.amount,
    ms.month_running_total,
    st.target_amount,
    ROUND(100 * ms.month_running_total / st.target_amount, 2) AS target_percentage
FROM monthly_sales ms
JOIN exrt_sales_targets st ON ms.month_year = st.month_year
ORDER BY ms.sale_date;
SALE_DATEAMOUNTMONTH_RUNNING_TOTALTARGET_AMOUNTTARGET_PERCENTAGE
05-JAN-251200.001200.00300040.00
08-JAN-2545.001245.00300041.50
10-JAN-2585.001330.00300044.33
15-JAN-25350.001680.00300056.00
18-JAN-251150.002830.00300094.33
20-JAN-2540.002870.00300095.67
22-JAN-25120.002990.00300099.67
25-JAN-25380.003370.003000112.33

Conclusion

Running totals in Oracle SQL serve as fundamental tools for cumulative analysis across various business domains, from financial reporting to inventory management.

Window functions, particularly the SUM() OVER() construct, provide the most efficient and flexible approach for calculating running totals in modern Oracle databases.

While alternative methods like self-joins and correlated subqueries exist, they should generally be avoided due to performance limitations on larger datasets.

Understanding partitioning, window frames, and conditional logic enables developers to create sophisticated running total calculations tailored to specific business requirements.

Proper indexing, materialized views, and query optimization techniques ensure that running total calculations remain performant even as data volumes grow.

By mastering these techniques, database professionals can deliver powerful analytical capabilities that transform raw transactional data into meaningful cumulative insights for decision-making.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE and software veteran with 25+ years of experience, passionate about AI and IT innovation.

guest

0 Comments
Oldest
Newest Most Voted